diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..eb8c9f1 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "config:best-practices", + "schedule:weekly", + ":combinePatchMinorReleases", + ":approveMajorUpdates" + ], + "labels": [ + "dependencies" + ] +} \ No newline at end of file diff --git a/.github/workflows/app.yaml b/.github/workflows/app.yaml new file mode 100644 index 0000000..4ff87d2 --- /dev/null +++ b/.github/workflows/app.yaml @@ -0,0 +1,96 @@ +--- +name: app + +on: [push] + +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.4' + 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.4' + 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.4' + 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.4' + cache-dependency-path: 'app/go.sum' + - run: task build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b018f68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# 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/ + +# Terraform +**/.terraform/* +*.tfstate +*.tfstate.* +*.tfvars +.terraform.tfstate.lock.info +!infra/spacelift/init/*.tfstate +!infra/spacelift/init/*.tfstate.* + +# Task +.task/ + +# Mise +.mise.local.toml +.mise.*.local.toml \ No newline at end of file diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..2dff477 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,13 @@ +[tools] +atlas = "0.29.0" +mkcert = "1.4.4" +go = "1.23.4" +golangci-lint = "1.62.2" +opentofu = "1.8.7" +protoc = "29.1" +protoc-gen-go = "1.35.2" +protoc-gen-go-grpc = "1.5.1" +task = "3.40.1" + +"go:github.com/sqlc-dev/sqlc/cmd/sqlc" = "1.27.0" +"go:goa.design/goa/v3/cmd/goa" = "3.19.1" diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..4770261 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,151 @@ +--- +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=local + DATABASE_CONNECTION_URI: postgresql://countup:countup@localhost:5432/countup + OAUTH_REDIRECT_URL: https://localhost:4043/login/google/callback + + 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/**/* + + gen:sqlc: + internal: true + deps: ['migration:plan'] + dir: app + cmds: + - sqlc generate + sources: + - sqlc.yaml + - schema/*.sql + + migration:new: + dir: app + cmds: + - atlas migrate new --env local {{ .NAME }} + requires: + vars: [NAME] + + migration:plan: + dir: app + cmds: + - atlas migrate diff --env local {{ .NAME }} + sources: + - schema/schema.sql + - schema/migrations/*.sql + generates: + - schema/migrations/*.sql + + migration:checksum: + dir: app + cmds: + - atlas migrate hash --env local + + 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 }} . + + certs:traefik: + dir: infra/environments/local/compose/traefik/certs + cmds: + - mkcert -install + - mkcert -cert-file traefik.cert -key-file traefik.key localhost 127.0.0.1 ::1 + status: + - test -f traefik.cert + - test -f traefik.key + + compose: + ignore_error: true + deps: ['gen', 'certs:traefik'] + cmds: + - docker compose --profile apps up --build {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:infra: + ignore_error: true + deps: ['gen', 'certs:traefik'] + cmds: + - docker compose up {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:down: + ignore_error: true + cmds: + - docker compose down -v --remove-orphans + + spacelift:init: + dir: infra/spacelift/init + cmds: + - tofu apply diff --git a/app/.golangci.yaml b/app/.golangci.yaml new file mode 100644 index 0000000..0878943 --- /dev/null +++ b/app/.golangci.yaml @@ -0,0 +1,139 @@ +--- +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: 60 + 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 + + wrapcheck: + ignoreSigs: + - .Errorf( + - errors.New( + - errors.Unwrap( + - errors.Join( + - .Wrap( + - .Wrapf( + - .WithMessage( + - .WithMessagef( + - .WithStack( + - (context.Context).Err() + - (*github.com/jace-ys/countup/internal/postgres.Pool).WithinTx + +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - copyloopvar + - cyclop + - decorder + - 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: + exclude-use-default: true + max-issues-per-linter: 0 + max-same-issues: 0 + + exclude-dirs: + - ^api/v[0-9]+/gen$ + - ^cmd/countup-cli$ + - ^internal/handler/teapot$ diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..a9a3053 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.23.4 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 gcr.io/distroless/static-debian12 +WORKDIR /app +USER nonroot:nonroot +COPY --from=builder --chown=nonroot:nonroot /src/dist /app/bin +ENTRYPOINT ["/app/bin/countup"] \ No newline at end of file diff --git a/app/api/v1/constants.go b/app/api/v1/constants.go new file mode 100644 index 0000000..7902b03 --- /dev/null +++ b/app/api/v1/constants.go @@ -0,0 +1,33 @@ +package apiv1 + +import ( + "embed" + + . "goa.design/goa/v3/dsl" +) + +//go:embed gen/http/*.json gen/http/*.yaml +var OpenAPIFS embed.FS + +const ( + AuthScopeAPIUser = "api.user" +) + +const ( + ErrCodeUnauthorized = "unauthorized" + ErrCodeForbidden = "forbidden" + ErrCodeIncompleteAuthInfo = "incomplete_auth_info" + ErrCodeExistingIncrementRequest = "existing_increment_request" +) + +const ( + CookieNameWebSession = "session_cookie:countup.session" +) + +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") +}) diff --git a/app/api/v1/countup.go b/app/api/v1/countup.go new file mode 100644 index 0000000..57e3968 --- /dev/null +++ b/app/api/v1/countup.go @@ -0,0 +1,268 @@ +package apiv1 + +import ( + . "goa.design/goa/v3/dsl" +) + +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", "teapot") + Host("local-http", func() { + URI("http://localhost:8080") + }) + Host("local-grpc", func() { + URI("grpc://localhost:8081") + }) + }) +}) + +var JWTAuth = JWTSecurity("jwt", func() { + Scope(AuthScopeAPIUser) +}) + +var _ = Service("api", func() { + Security(JWTAuth, func() { + Scope(AuthScopeAPIUser) + }) + + Error(ErrCodeUnauthorized) + Error(ErrCodeForbidden) + + HTTP(func() { + Path("/api/v1") + Response(ErrCodeUnauthorized, StatusUnauthorized) + Response(ErrCodeForbidden, StatusForbidden) + }) + + GRPC(func() { + Response(ErrCodeUnauthorized, CodeUnauthenticated) + Response(ErrCodeForbidden, CodePermissionDenied) + }) + + Method("AuthToken", func() { + NoSecurity() + + Error(ErrCodeIncompleteAuthInfo) + + Payload(func() { + Field(1, "provider", String, func() { + Enum("google") + }) + Field(2, "access_token", String) + Required("provider", "access_token") + }) + + Result(func() { + Field(1, "token", String) + Required("token") + }) + + HTTP(func() { + POST("/auth/token") + Response(StatusOK) + Response(ErrCodeIncompleteAuthInfo, StatusUnauthorized) + }) + + GRPC(func() { + Response(CodeOK) + Response(ErrCodeIncompleteAuthInfo, CodePermissionDenied) + }) + }) + + Method("CounterGet", func() { + NoSecurity() + + Result(CounterInfo) + + HTTP(func() { + GET("/counter") + Response(StatusOK) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + Method("CounterIncrement", func() { + Error(ErrCodeExistingIncrementRequest, func() { + Temporary() + }) + + Payload(func() { + TokenField(1, "token", String) + }) + + Result(CounterInfo) + + HTTP(func() { + POST("/counter") + Response(StatusAccepted) + Response(ErrCodeExistingIncrementRequest, StatusTooManyRequests) + }) + + GRPC(func() { + Response(CodeOK) + Response(ErrCodeExistingIncrementRequest, CodeAlreadyExists) + }) + }) + + Files("/openapi.json", "gen/http/openapi3.json") +}) + +var _ = Service("web", func() { + Error(ErrCodeUnauthorized) + + HTTP(func() { + Path("/") + Response(ErrCodeUnauthorized, StatusUnauthorized) + }) + + withSessionCookie := func() { + Cookie(CookieNameWebSession) + CookieSameSite(CookieSameSiteLax) + CookieMaxAge(86400) + CookieHTTPOnly() + CookieSecure() + CookiePath("/") + } + + 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") + }) + }) + }) + + Method("LoginGoogle", func() { + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/login/google") + Response(StatusFound, func() { + Header("redirect_url:Location", String) + withSessionCookie() + }) + }) + }) + + Method("LoginGoogleCallback", func() { + Payload(func() { + Attribute("code", String) + Attribute("state", String) + Attribute("session_cookie", String) + Required("code", "state", "session_cookie") + }) + + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/login/google/callback") + Params(func() { + Param("code", String) + Param("state", String) + Required("code", "state") + }) + Cookie(CookieNameWebSession) + Response(StatusFound, func() { + Header("redirect_url:Location", String) + withSessionCookie() + }) + }) + }) + + Method("Logout", func() { + Payload(func() { + Attribute("session_cookie", String) + Required("session_cookie") + }) + + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/logout") + Cookie(CookieNameWebSession) + Response(StatusFound, func() { + Header("redirect_url:Location", String) + withSessionCookie() + }) + }) + }) + + Method("SessionToken", func() { + Payload(func() { + Attribute("session_cookie", String) + Required("session_cookie") + }) + + Result(func() { + Attribute("token", String) + Required("token") + }) + + HTTP(func() { + GET("/session/token") + Cookie(CookieNameWebSession) + Response(StatusOK, func() { + ContentType("application/json") + }) + }) + }) + + Files("/static/*", "static/") +}) + +var _ = Service("teapot", func() { + Error("unwell") + + 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") +}) diff --git a/app/api/v1/gen/api/client.go b/app/api/v1/gen/api/client.go new file mode 100644 index 0000000..70aac8c --- /dev/null +++ b/app/api/v1/gen/api/client.go @@ -0,0 +1,74 @@ +// 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 { + AuthTokenEndpoint goa.Endpoint + CounterGetEndpoint goa.Endpoint + CounterIncrementEndpoint goa.Endpoint +} + +// NewClient initializes a "api" service client given the endpoints. +func NewClient(authToken, counterGet, counterIncrement goa.Endpoint) *Client { + return &Client{ + AuthTokenEndpoint: authToken, + CounterGetEndpoint: counterGet, + CounterIncrementEndpoint: counterIncrement, + } +} + +// AuthToken calls the "AuthToken" endpoint of the "api" service. +// AuthToken may return the following errors: +// - "incomplete_auth_info" (type *goa.ServiceError) +// - "unauthorized" (type *goa.ServiceError) +// - "forbidden" (type *goa.ServiceError) +// - error: internal error +func (c *Client) AuthToken(ctx context.Context, p *AuthTokenPayload) (res *AuthTokenResult, err error) { + var ires any + ires, err = c.AuthTokenEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*AuthTokenResult), nil +} + +// CounterGet calls the "CounterGet" endpoint of the "api" service. +// CounterGet may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "forbidden" (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: +// - "existing_increment_request" (type *goa.ServiceError) +// - "unauthorized" (type *goa.ServiceError) +// - "forbidden" (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 +} diff --git a/app/api/v1/gen/api/endpoints.go b/app/api/v1/gen/api/endpoints.go new file mode 100644 index 0000000..f3dbb93 --- /dev/null +++ b/app/api/v1/gen/api/endpoints.go @@ -0,0 +1,90 @@ +// 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" + "goa.design/goa/v3/security" +) + +// Endpoints wraps the "api" service endpoints. +type Endpoints struct { + AuthToken goa.Endpoint + CounterGet goa.Endpoint + CounterIncrement goa.Endpoint +} + +// NewEndpoints wraps the methods of the "api" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + // Casting service to Auther interface + a := s.(Auther) + return &Endpoints{ + AuthToken: NewAuthTokenEndpoint(s), + CounterGet: NewCounterGetEndpoint(s), + CounterIncrement: NewCounterIncrementEndpoint(s, a.JWTAuth), + } +} + +// Use applies the given middleware to all the "api" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.AuthToken = m(e.AuthToken) + e.CounterGet = m(e.CounterGet) + e.CounterIncrement = m(e.CounterIncrement) +} + +// NewAuthTokenEndpoint returns an endpoint function that calls the method +// "AuthToken" of service "api". +func NewAuthTokenEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*AuthTokenPayload) + return s.AuthToken(ctx, p) + } +} + +// 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, authJWTFn security.AuthJWTFunc) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*CounterIncrementPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{"api.user"}, + RequiredScopes: []string{"api.user"}, + } + var token string + if p.Token != nil { + token = *p.Token + } + ctx, err = authJWTFn(ctx, token, &sc) + if err != nil { + return nil, err + } + res, err := s.CounterIncrement(ctx, p) + if err != nil { + return nil, err + } + vres := NewViewedCounterInfo(res, "default") + return vres, nil + } +} diff --git a/app/api/v1/gen/api/service.go b/app/api/v1/gen/api/service.go new file mode 100644 index 0000000..d2688fb --- /dev/null +++ b/app/api/v1/gen/api/service.go @@ -0,0 +1,137 @@ +// 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" + "goa.design/goa/v3/security" +) + +// Service is the api service interface. +type Service interface { + // AuthToken implements AuthToken. + AuthToken(context.Context, *AuthTokenPayload) (res *AuthTokenResult, err error) + // CounterGet implements CounterGet. + CounterGet(context.Context) (res *CounterInfo, err error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementPayload) (res *CounterInfo, err error) +} + +// Auther defines the authorization functions to be implemented by the service. +type Auther interface { + // JWTAuth implements the authorization logic for the JWT security scheme. + JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, 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{"AuthToken", "CounterGet", "CounterIncrement"} + +// AuthTokenPayload is the payload type of the api service AuthToken method. +type AuthTokenPayload struct { + Provider string + AccessToken string +} + +// AuthTokenResult is the result type of the api service AuthToken method. +type AuthTokenResult struct { + Token string +} + +// CounterIncrementPayload is the payload type of the api service +// CounterIncrement method. +type CounterIncrementPayload struct { + Token *string +} + +// CounterInfo is the result type of the api service CounterGet method. +type CounterInfo struct { + Count int32 + LastIncrementBy string + LastIncrementAt string + NextFinalizeAt string +} + +// MakeUnauthorized builds a goa.ServiceError from an error. +func MakeUnauthorized(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unauthorized", false, false, false) +} + +// MakeForbidden builds a goa.ServiceError from an error. +func MakeForbidden(err error) *goa.ServiceError { + return goa.NewServiceError(err, "forbidden", false, false, false) +} + +// MakeIncompleteAuthInfo builds a goa.ServiceError from an error. +func MakeIncompleteAuthInfo(err error) *goa.ServiceError { + return goa.NewServiceError(err, "incomplete_auth_info", 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..e744c2a --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/cli.go @@ -0,0 +1,52 @@ +// 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" +) + +// BuildAuthTokenPayload builds the payload for the api AuthToken endpoint from +// CLI flags. +func BuildAuthTokenPayload(apiAuthTokenMessage string) (*api.AuthTokenPayload, error) { + var err error + var message apipb.AuthTokenRequest + { + if apiAuthTokenMessage != "" { + err = json.Unmarshal([]byte(apiAuthTokenMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"access_token\": \"Est sunt voluptatem reprehenderit neque.\",\n \"provider\": \"google\"\n }'") + } + } + } + v := &api.AuthTokenPayload{ + Provider: message.Provider, + AccessToken: message.AccessToken, + } + + return v, nil +} + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementToken string) (*api.CounterIncrementPayload, error) { + var token *string + { + if apiCounterIncrementToken != "" { + token = &apiCounterIncrementToken + } + } + v := &api.CounterIncrementPayload{} + v.Token = token + + 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..f08ac6e --- /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, + } +} // AuthToken calls the "AuthToken" function in apipb.APIClient interface. +func (c *Client) AuthToken() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildAuthTokenFunc(c.grpccli, c.opts...), + EncodeAuthTokenRequest, + DecodeAuthTokenResponse) + 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 + } +} // 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 + } +} 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..9e4e2ce --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/encode_decode.go @@ -0,0 +1,130 @@ +// 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" +) + +// BuildAuthTokenFunc builds the remote method to invoke for "api" service +// "AuthToken" endpoint. +func BuildAuthTokenFunc(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.AuthToken(ctx, reqpb.(*apipb.AuthTokenRequest), opts...) + } + return grpccli.AuthToken(ctx, &apipb.AuthTokenRequest{}, opts...) + } +} + +// EncodeAuthTokenRequest encodes requests sent to api AuthToken endpoint. +func EncodeAuthTokenRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.AuthTokenPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*api.AuthTokenPayload", v) + } + return NewProtoAuthTokenRequest(payload), nil +} + +// DecodeAuthTokenResponse decodes responses from the api AuthToken endpoint. +func DecodeAuthTokenResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + message, ok := v.(*apipb.AuthTokenResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*apipb.AuthTokenResponse", v) + } + res := NewAuthTokenResult(message) + return res, nil +} // 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) + } + if payload.Token != nil { + (*md).Append("authorization", *payload.Token) + } + return NewProtoCounterIncrementRequest(), 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 +} 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..b407715 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/types.go @@ -0,0 +1,71 @@ +// 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" +) + +// NewProtoAuthTokenRequest builds the gRPC request type from the payload of +// the "AuthToken" endpoint of the "api" service. +func NewProtoAuthTokenRequest(payload *api.AuthTokenPayload) *apipb.AuthTokenRequest { + message := &apipb.AuthTokenRequest{ + Provider: payload.Provider, + AccessToken: payload.AccessToken, + } + return message +} + +// NewAuthTokenResult builds the result type of the "AuthToken" endpoint of the +// "api" service from the gRPC response type. +func NewAuthTokenResult(message *apipb.AuthTokenResponse) *api.AuthTokenResult { + result := &api.AuthTokenResult{ + Token: message.Token, + } + return result +} + +// 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() *apipb.CounterIncrementRequest { + message := &apipb.CounterIncrementRequest{} + 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 +} 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..5c3c291 --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go @@ -0,0 +1,451 @@ +// 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.2 +// protoc v5.29.1 +// 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 AuthTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` +} + +func (x *AuthTokenRequest) Reset() { + *x = AuthTokenRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthTokenRequest) ProtoMessage() {} + +func (x *AuthTokenRequest) 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 AuthTokenRequest.ProtoReflect.Descriptor instead. +func (*AuthTokenRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthTokenRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *AuthTokenRequest) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +type AuthTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *AuthTokenResponse) Reset() { + *x = AuthTokenResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthTokenResponse) ProtoMessage() {} + +func (x *AuthTokenResponse) 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 AuthTokenResponse.ProtoReflect.Descriptor instead. +func (*AuthTokenResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +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[2] + 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[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 CounterGetRequest.ProtoReflect.Descriptor instead. +func (*CounterGetRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{2} +} + +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[3] + 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[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 CounterGetResponse.ProtoReflect.Descriptor instead. +func (*CounterGetResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{3} +} + +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 +} + +func (x *CounterIncrementRequest) Reset() { + *x = CounterIncrementRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[4] + 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[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 CounterIncrementRequest.ProtoReflect.Descriptor instead. +func (*CounterIncrementRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{4} +} + +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[5] + 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[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 CounterIncrementResponse.ProtoReflect.Descriptor instead. +func (*CounterIncrementResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{5} +} + +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 "" +} + +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, 0x51, 0x0a, 0x10, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x29, 0x0a, + 0x11, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 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, 0x19, 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, 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, 0x32, 0xd1, 0x01, 0x0a, + 0x03, 0x41, 0x50, 0x49, 0x12, 0x3a, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 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, + 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{ + (*AuthTokenRequest)(nil), // 0: api.AuthTokenRequest + (*AuthTokenResponse)(nil), // 1: api.AuthTokenResponse + (*CounterGetRequest)(nil), // 2: api.CounterGetRequest + (*CounterGetResponse)(nil), // 3: api.CounterGetResponse + (*CounterIncrementRequest)(nil), // 4: api.CounterIncrementRequest + (*CounterIncrementResponse)(nil), // 5: api.CounterIncrementResponse +} +var file_goagen_v1_api_proto_depIdxs = []int32{ + 0, // 0: api.API.AuthToken:input_type -> api.AuthTokenRequest + 2, // 1: api.API.CounterGet:input_type -> api.CounterGetRequest + 4, // 2: api.API.CounterIncrement:input_type -> api.CounterIncrementRequest + 1, // 3: api.API.AuthToken:output_type -> api.AuthTokenResponse + 3, // 4: api.API.CounterGet:output_type -> api.CounterGetResponse + 5, // 5: api.API.CounterIncrement:output_type -> api.CounterIncrementResponse + 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..66d283e --- /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 { + // AuthToken implements AuthToken. + rpc AuthToken (AuthTokenRequest) returns (AuthTokenResponse); + // CounterGet implements CounterGet. + rpc CounterGet (CounterGetRequest) returns (CounterGetResponse); + // CounterIncrement implements CounterIncrement. + rpc CounterIncrement (CounterIncrementRequest) returns (CounterIncrementResponse); +} + +message AuthTokenRequest { + string provider = 1; + string access_token = 2; +} + +message AuthTokenResponse { + string token = 1; +} + +message CounterGetRequest { +} + +message CounterGetResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} + +message CounterIncrementRequest { +} + +message CounterIncrementResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} 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..ae5d734 --- /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.29.1 +// 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_AuthToken_FullMethodName = "/api.API/AuthToken" + API_CounterGet_FullMethodName = "/api.API/CounterGet" + API_CounterIncrement_FullMethodName = "/api.API/CounterIncrement" +) + +// 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 { + // AuthToken implements AuthToken. + AuthToken(ctx context.Context, in *AuthTokenRequest, opts ...grpc.CallOption) (*AuthTokenResponse, error) + // 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) +} + +type aPIClient struct { + cc grpc.ClientConnInterface +} + +func NewAPIClient(cc grpc.ClientConnInterface) APIClient { + return &aPIClient{cc} +} + +func (c *aPIClient) AuthToken(ctx context.Context, in *AuthTokenRequest, opts ...grpc.CallOption) (*AuthTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthTokenResponse) + err := c.cc.Invoke(ctx, API_AuthToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +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 +} + +// 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 { + // AuthToken implements AuthToken. + AuthToken(context.Context, *AuthTokenRequest) (*AuthTokenResponse, error) + // CounterGet implements CounterGet. + CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementRequest) (*CounterIncrementResponse, 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) AuthToken(context.Context, *AuthTokenRequest) (*AuthTokenResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AuthToken not implemented") +} +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) 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_AuthToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).AuthToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_AuthToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).AuthToken(ctx, req.(*AuthTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +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) +} + +// 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: "AuthToken", + Handler: _API_AuthToken_Handler, + }, + { + MethodName: "CounterGet", + Handler: _API_CounterGet_Handler, + }, + { + MethodName: "CounterIncrement", + Handler: _API_CounterIncrement_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..ad87d9a --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/encode_decode.go @@ -0,0 +1,107 @@ +// 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" + "strings" + + 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" +) + +// EncodeAuthTokenResponse encodes responses from the "api" service "AuthToken" +// endpoint. +func EncodeAuthTokenResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + result, ok := v.(*api.AuthTokenResult) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*api.AuthTokenResult", v) + } + resp := NewProtoAuthTokenResponse(result) + return resp, nil +} + +// DecodeAuthTokenRequest decodes requests sent to "api" service "AuthToken" +// endpoint. +func DecodeAuthTokenRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *apipb.AuthTokenRequest + ok bool + ) + { + if message, ok = v.(*apipb.AuthTokenRequest); !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*apipb.AuthTokenRequest", v) + } + if err := ValidateAuthTokenRequest(message); err != nil { + return nil, err + } + } + var payload *api.AuthTokenPayload + { + payload = NewAuthTokenPayload(message) + } + return payload, nil +} + +// 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 ( + token *string + err error + ) + { + if vals := md.Get("authorization"); len(vals) > 0 { + token = &vals[0] + } + } + if err != nil { + return nil, err + } + var payload *api.CounterIncrementPayload + { + payload = NewCounterIncrementPayload(token) + if payload.Token != nil { + if strings.Contains(*payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.Token, " ", 2)[1] + payload.Token = &cred + } + } + } + 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..705d5bf --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/server.go @@ -0,0 +1,128 @@ +// 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 { + AuthTokenH goagrpc.UnaryHandler + CounterGetH goagrpc.UnaryHandler + CounterIncrementH 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{ + AuthTokenH: NewAuthTokenHandler(e.AuthToken, uh), + CounterGetH: NewCounterGetHandler(e.CounterGet, uh), + CounterIncrementH: NewCounterIncrementHandler(e.CounterIncrement, uh), + } +} + +// NewAuthTokenHandler creates a gRPC handler which serves the "api" service +// "AuthToken" endpoint. +func NewAuthTokenHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, DecodeAuthTokenRequest, EncodeAuthTokenResponse) + } + return h +} + +// AuthToken implements the "AuthToken" method in apipb.APIServer interface. +func (s *Server) AuthToken(ctx context.Context, message *apipb.AuthTokenRequest) (*apipb.AuthTokenResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "AuthToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.AuthTokenH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "incomplete_auth_info": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.Unauthenticated, err, goagrpc.NewErrorResponse(err)) + case "forbidden": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.AuthTokenResponse), nil +} + +// 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.Unauthenticated, err, goagrpc.NewErrorResponse(err)) + case "forbidden": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, 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 "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.Unauthenticated, err, goagrpc.NewErrorResponse(err)) + case "forbidden": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.CounterIncrementResponse), 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..7b3a921 --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/types.go @@ -0,0 +1,74 @@ +// 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" + goa "goa.design/goa/v3/pkg" +) + +// NewAuthTokenPayload builds the payload of the "AuthToken" endpoint of the +// "api" service from the gRPC request type. +func NewAuthTokenPayload(message *apipb.AuthTokenRequest) *api.AuthTokenPayload { + v := &api.AuthTokenPayload{ + Provider: message.Provider, + AccessToken: message.AccessToken, + } + return v +} + +// NewProtoAuthTokenResponse builds the gRPC response type from the result of +// the "AuthToken" endpoint of the "api" service. +func NewProtoAuthTokenResponse(result *api.AuthTokenResult) *apipb.AuthTokenResponse { + message := &apipb.AuthTokenResponse{ + Token: result.Token, + } + return message +} + +// 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(token *string) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{} + v.Token = token + 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 +} + +// ValidateAuthTokenRequest runs the validations defined on AuthTokenRequest. +func ValidateAuthTokenRequest(message *apipb.AuthTokenRequest) (err error) { + if !(message.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("message.provider", message.Provider, []any{"google"})) + } + return +} 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..e3ff6d9 --- /dev/null +++ b/app/api/v1/gen/grpc/cli/countup/cli.go @@ -0,0 +1,243 @@ +// 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" + teapotc "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/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 (auth-token|counter-get|counter-increment) +teapot echo +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api auth-token --message '{ + "access_token": "Est sunt voluptatem reprehenderit neque.", + "provider": "google" + }'` + "\n" + + os.Args[0] + ` teapot echo --message '{ + "text": "Assumenda mollitia deleniti expedita." + }'` + "\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) + + apiAuthTokenFlags = flag.NewFlagSet("auth-token", flag.ExitOnError) + apiAuthTokenMessageFlag = apiAuthTokenFlags.String("message", "", "") + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementTokenFlag = apiCounterIncrementFlags.String("token", "", "") + + teapotFlags = flag.NewFlagSet("teapot", flag.ContinueOnError) + + teapotEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + teapotEchoMessageFlag = teapotEchoFlags.String("message", "", "") + ) + apiFlags.Usage = apiUsage + apiAuthTokenFlags.Usage = apiAuthTokenUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + + teapotFlags.Usage = teapotUsage + teapotEchoFlags.Usage = teapotEchoUsage + + 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 "teapot": + svcf = teapotFlags + 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 "auth-token": + epf = apiAuthTokenFlags + + case "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + } + + case "teapot": + switch epn { + case "echo": + epf = teapotEchoFlags + + } + + } + } + 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 "auth-token": + endpoint = c.AuthToken() + data, err = apic.BuildAuthTokenPayload(*apiAuthTokenMessageFlag) + case "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementTokenFlag) + } + case "teapot": + c := teapotc.NewClient(cc, opts...) + switch epn { + case "echo": + endpoint = c.Echo() + data, err = teapotc.BuildEchoPayload(*teapotEchoMessageFlag) + } + } + } + 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: + auth-token: AuthToken implements AuthToken. + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + +Additional help: + %[1]s api COMMAND --help +`, os.Args[0]) +} +func apiAuthTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api auth-token -message JSON + +AuthToken implements AuthToken. + -message JSON: + +Example: + %[1]s api auth-token --message '{ + "access_token": "Est sunt voluptatem reprehenderit neque.", + "provider": "google" + }' +`, 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 -token STRING + +CounterIncrement implements CounterIncrement. + -token STRING: + +Example: + %[1]s api counter-increment --token "Minima dolorem id." +`, os.Args[0]) +} + +// teapotUsage displays the usage of the teapot command and its subcommands. +func teapotUsage() { + fmt.Fprintf(os.Stderr, `Service is the teapot service interface. +Usage: + %[1]s [globalflags] teapot COMMAND [flags] + +COMMAND: + echo: Echo implements Echo. + +Additional help: + %[1]s teapot COMMAND --help +`, os.Args[0]) +} +func teapotEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] teapot echo -message JSON + +Echo implements Echo. + -message JSON: + +Example: + %[1]s teapot echo --message '{ + "text": "Assumenda mollitia deleniti expedita." + }' +`, os.Args[0]) +} diff --git a/app/api/v1/gen/grpc/teapot/client/cli.go b/app/api/v1/gen/grpc/teapot/client/cli.go new file mode 100644 index 0000000..e2f599d --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/cli.go @@ -0,0 +1,36 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// BuildEchoPayload builds the payload for the teapot Echo endpoint from CLI +// flags. +func BuildEchoPayload(teapotEchoMessage string) (*teapot.EchoPayload, error) { + var err error + var message teapotpb.EchoRequest + { + if teapotEchoMessage != "" { + err = json.Unmarshal([]byte(teapotEchoMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Assumenda mollitia deleniti expedita.\"\n }'") + } + } + } + v := &teapot.EchoPayload{ + Text: message.Text, + } + + return v, nil +} diff --git a/app/api/v1/gen/grpc/teapot/client/client.go b/app/api/v1/gen/grpc/teapot/client/client.go new file mode 100644 index 0000000..76fc289 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/client.go @@ -0,0 +1,42 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + goagrpc "goa.design/goa/v3/grpc" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc" +) + +// Client lists the service endpoint gRPC clients. +type Client struct { + grpccli teapotpb.TeapotClient + opts []grpc.CallOption +} // NewClient instantiates gRPC client for all the teapot service servers. +func NewClient(cc *grpc.ClientConn, opts ...grpc.CallOption) *Client { + return &Client{ + grpccli: teapotpb.NewTeapotClient(cc), + opts: opts, + } +} // Echo calls the "Echo" function in teapotpb.TeapotClient 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 { + return nil, goa.Fault(err.Error()) + } + return res, nil + } +} diff --git a/app/api/v1/gen/grpc/teapot/client/encode_decode.go b/app/api/v1/gen/grpc/teapot/client/encode_decode.go new file mode 100644 index 0000000..a56daec --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/encode_decode.go @@ -0,0 +1,51 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// BuildEchoFunc builds the remote method to invoke for "teapot" service "Echo" +// endpoint. +func BuildEchoFunc(grpccli teapotpb.TeapotClient, 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.(*teapotpb.EchoRequest), opts...) + } + return grpccli.Echo(ctx, &teapotpb.EchoRequest{}, opts...) + } +} + +// EncodeEchoRequest encodes requests sent to teapot Echo endpoint. +func EncodeEchoRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*teapot.EchoPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapot.EchoPayload", v) + } + return NewProtoEchoRequest(payload), nil +} + +// DecodeEchoResponse decodes responses from the teapot Echo endpoint. +func DecodeEchoResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + message, ok := v.(*teapotpb.EchoResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapotpb.EchoResponse", v) + } + res := NewEchoResult(message) + return res, nil +} diff --git a/app/api/v1/gen/grpc/teapot/client/types.go b/app/api/v1/gen/grpc/teapot/client/types.go new file mode 100644 index 0000000..369d39e --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/types.go @@ -0,0 +1,31 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// NewProtoEchoRequest builds the gRPC request type from the payload of the +// "Echo" endpoint of the "teapot" service. +func NewProtoEchoRequest(payload *teapot.EchoPayload) *teapotpb.EchoRequest { + message := &teapotpb.EchoRequest{ + Text: payload.Text, + } + return message +} + +// NewEchoResult builds the result type of the "Echo" endpoint of the "teapot" +// service from the gRPC response type. +func NewEchoResult(message *teapotpb.EchoResponse) *teapot.EchoResult { + result := &teapot.EchoResult{ + Text: message.Text, + } + return result +} diff --git a/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.pb.go b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.pb.go new file mode 100644 index 0000000..7a22f81 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.pb.go @@ -0,0 +1,187 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// teapot 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.2 +// protoc v5.29.1 +// source: goagen_v1_teapot.proto + +package teapotpb + +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 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_teapot_proto_msgTypes[0] + 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_teapot_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 EchoRequest.ProtoReflect.Descriptor instead. +func (*EchoRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_teapot_proto_rawDescGZIP(), []int{0} +} + +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_teapot_proto_msgTypes[1] + 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_teapot_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 EchoResponse.ProtoReflect.Descriptor instead. +func (*EchoResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_teapot_proto_rawDescGZIP(), []int{1} +} + +func (x *EchoResponse) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +var File_goagen_v1_teapot_proto protoreflect.FileDescriptor + +var file_goagen_v1_teapot_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x67, 0x6f, 0x61, 0x67, 0x65, 0x6e, 0x5f, 0x76, 0x31, 0x5f, 0x74, 0x65, 0x61, 0x70, + 0x6f, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x74, 0x65, 0x61, 0x70, 0x6f, 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, 0x3b, 0x0a, 0x06, 0x54, 0x65, 0x61, 0x70, 0x6f, + 0x74, 0x12, 0x31, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x13, 0x2e, 0x74, 0x65, 0x61, 0x70, + 0x6f, 0x74, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, + 0x2e, 0x74, 0x65, 0x61, 0x70, 0x6f, 0x74, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x2f, 0x74, 0x65, 0x61, 0x70, 0x6f, 0x74, 0x70, + 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_goagen_v1_teapot_proto_rawDescOnce sync.Once + file_goagen_v1_teapot_proto_rawDescData = file_goagen_v1_teapot_proto_rawDesc +) + +func file_goagen_v1_teapot_proto_rawDescGZIP() []byte { + file_goagen_v1_teapot_proto_rawDescOnce.Do(func() { + file_goagen_v1_teapot_proto_rawDescData = protoimpl.X.CompressGZIP(file_goagen_v1_teapot_proto_rawDescData) + }) + return file_goagen_v1_teapot_proto_rawDescData +} + +var file_goagen_v1_teapot_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_goagen_v1_teapot_proto_goTypes = []any{ + (*EchoRequest)(nil), // 0: teapot.EchoRequest + (*EchoResponse)(nil), // 1: teapot.EchoResponse +} +var file_goagen_v1_teapot_proto_depIdxs = []int32{ + 0, // 0: teapot.Teapot.Echo:input_type -> teapot.EchoRequest + 1, // 1: teapot.Teapot.Echo:output_type -> teapot.EchoResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] 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_teapot_proto_init() } +func file_goagen_v1_teapot_proto_init() { + if File_goagen_v1_teapot_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_goagen_v1_teapot_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_goagen_v1_teapot_proto_goTypes, + DependencyIndexes: file_goagen_v1_teapot_proto_depIdxs, + MessageInfos: file_goagen_v1_teapot_proto_msgTypes, + }.Build() + File_goagen_v1_teapot_proto = out.File + file_goagen_v1_teapot_proto_rawDesc = nil + file_goagen_v1_teapot_proto_goTypes = nil + file_goagen_v1_teapot_proto_depIdxs = nil +} diff --git a/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.proto b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.proto new file mode 100644 index 0000000..664824e --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.proto @@ -0,0 +1,26 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// teapot protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +syntax = "proto3"; + +package teapot; + +option go_package = "/teapotpb"; + +// Service is the teapot service interface. +service Teapot { + // Echo implements Echo. + rpc Echo (EchoRequest) returns (EchoResponse); +} + +message EchoRequest { + string text = 1; +} + +message EchoResponse { + string text = 1; +} diff --git a/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot_grpc.pb.go b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot_grpc.pb.go new file mode 100644 index 0000000..16b7501 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot_grpc.pb.go @@ -0,0 +1,134 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// teapot 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.29.1 +// source: goagen_v1_teapot.proto + +package teapotpb + +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 ( + Teapot_Echo_FullMethodName = "/teapot.Teapot/Echo" +) + +// TeapotClient is the client API for Teapot 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 teapot service interface. +type TeapotClient interface { + // Echo implements Echo. + Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) +} + +type teapotClient struct { + cc grpc.ClientConnInterface +} + +func NewTeapotClient(cc grpc.ClientConnInterface) TeapotClient { + return &teapotClient{cc} +} + +func (c *teapotClient) 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, Teapot_Echo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TeapotServer is the server API for Teapot service. +// All implementations must embed UnimplementedTeapotServer +// for forward compatibility. +// +// Service is the teapot service interface. +type TeapotServer interface { + // Echo implements Echo. + Echo(context.Context, *EchoRequest) (*EchoResponse, error) + mustEmbedUnimplementedTeapotServer() +} + +// UnimplementedTeapotServer 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 UnimplementedTeapotServer struct{} + +func (UnimplementedTeapotServer) Echo(context.Context, *EchoRequest) (*EchoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") +} +func (UnimplementedTeapotServer) mustEmbedUnimplementedTeapotServer() {} +func (UnimplementedTeapotServer) testEmbeddedByValue() {} + +// UnsafeTeapotServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TeapotServer will +// result in compilation errors. +type UnsafeTeapotServer interface { + mustEmbedUnimplementedTeapotServer() +} + +func RegisterTeapotServer(s grpc.ServiceRegistrar, srv TeapotServer) { + // If the following call pancis, it indicates UnimplementedTeapotServer 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(&Teapot_ServiceDesc, srv) +} + +func _Teapot_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.(TeapotServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Teapot_Echo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TeapotServer).Echo(ctx, req.(*EchoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Teapot_ServiceDesc is the grpc.ServiceDesc for Teapot service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Teapot_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "teapot.Teapot", + HandlerType: (*TeapotServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Echo", + Handler: _Teapot_Echo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "goagen_v1_teapot.proto", +} diff --git a/app/api/v1/gen/grpc/teapot/server/encode_decode.go b/app/api/v1/gen/grpc/teapot/server/encode_decode.go new file mode 100644 index 0000000..b1dc5d2 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/server/encode_decode.go @@ -0,0 +1,46 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc/metadata" +) + +// EncodeEchoResponse encodes responses from the "teapot" service "Echo" +// endpoint. +func EncodeEchoResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + result, ok := v.(*teapot.EchoResult) + if !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapot.EchoResult", v) + } + resp := NewProtoEchoResponse(result) + return resp, nil +} + +// DecodeEchoRequest decodes requests sent to "teapot" service "Echo" endpoint. +func DecodeEchoRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *teapotpb.EchoRequest + ok bool + ) + { + if message, ok = v.(*teapotpb.EchoRequest); !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapotpb.EchoRequest", v) + } + } + var payload *teapot.EchoPayload + { + payload = NewEchoPayload(message) + } + return payload, nil +} diff --git a/app/api/v1/gen/grpc/teapot/server/server.go b/app/api/v1/gen/grpc/teapot/server/server.go new file mode 100644 index 0000000..ac1bcc4 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/server/server.go @@ -0,0 +1,50 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goagrpc "goa.design/goa/v3/grpc" + goa "goa.design/goa/v3/pkg" +) + +// Server implements the teapotpb.TeapotServer interface. +type Server struct { + EchoH goagrpc.UnaryHandler + teapotpb.UnimplementedTeapotServer +} + +// New instantiates the server struct with the teapot service endpoints. +func New(e *teapot.Endpoints, uh goagrpc.UnaryHandler) *Server { + return &Server{ + EchoH: NewEchoHandler(e.Echo, uh), + } +} + +// NewEchoHandler creates a gRPC handler which serves the "teapot" 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 teapotpb.TeapotServer interface. +func (s *Server) Echo(ctx context.Context, message *teapotpb.EchoRequest) (*teapotpb.EchoResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "Echo") + ctx = context.WithValue(ctx, goa.ServiceKey, "teapot") + resp, err := s.EchoH.Handle(ctx, message) + if err != nil { + return nil, goagrpc.EncodeError(err) + } + return resp.(*teapotpb.EchoResponse), nil +} diff --git a/app/api/v1/gen/grpc/teapot/server/types.go b/app/api/v1/gen/grpc/teapot/server/types.go new file mode 100644 index 0000000..284ed0b --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/server/types.go @@ -0,0 +1,31 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// NewEchoPayload builds the payload of the "Echo" endpoint of the "teapot" +// service from the gRPC request type. +func NewEchoPayload(message *teapotpb.EchoRequest) *teapot.EchoPayload { + v := &teapot.EchoPayload{ + Text: message.Text, + } + return v +} + +// NewProtoEchoResponse builds the gRPC response type from the result of the +// "Echo" endpoint of the "teapot" service. +func NewProtoEchoResponse(result *teapot.EchoResult) *teapotpb.EchoResponse { + message := &teapotpb.EchoResponse{ + Text: result.Text, + } + return message +} 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..0856ab4 --- /dev/null +++ b/app/api/v1/gen/http/api/client/cli.go @@ -0,0 +1,56 @@ +// 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" + goa "goa.design/goa/v3/pkg" +) + +// BuildAuthTokenPayload builds the payload for the api AuthToken endpoint from +// CLI flags. +func BuildAuthTokenPayload(apiAuthTokenBody string) (*api.AuthTokenPayload, error) { + var err error + var body AuthTokenRequestBody + { + err = json.Unmarshal([]byte(apiAuthTokenBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"access_token\": \"Itaque unde qui ut molestiae et omnis.\",\n \"provider\": \"google\"\n }'") + } + if !(body.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("body.provider", body.Provider, []any{"google"})) + } + if err != nil { + return nil, err + } + } + v := &api.AuthTokenPayload{ + Provider: body.Provider, + AccessToken: body.AccessToken, + } + + return v, nil +} + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementToken string) (*api.CounterIncrementPayload, error) { + var token *string + { + if apiCounterIncrementToken != "" { + token = &apiCounterIncrementToken + } + } + v := &api.CounterIncrementPayload{} + v.Token = token + + 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..6d15d36 --- /dev/null +++ b/app/api/v1/gen/http/api/client/client.go @@ -0,0 +1,128 @@ +// 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 { + // AuthToken Doer is the HTTP client used to make requests to the AuthToken + // endpoint. + AuthTokenDoer goahttp.Doer + + // 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 + + // 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{ + AuthTokenDoer: doer, + CounterGetDoer: doer, + CounterIncrementDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// AuthToken returns an endpoint that makes HTTP requests to the api service +// AuthToken server. +func (c *Client) AuthToken() goa.Endpoint { + var ( + encodeRequest = EncodeAuthTokenRequest(c.encoder) + decodeResponse = DecodeAuthTokenResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildAuthTokenRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.AuthTokenDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "AuthToken", err) + } + return decodeResponse(resp) + } +} + +// 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) + } +} 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..ed587ad --- /dev/null +++ b/app/api/v1/gen/http/api/client/encode_decode.go @@ -0,0 +1,359 @@ +// 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" + "strings" + + 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" +) + +// BuildAuthTokenRequest instantiates a HTTP request object with method and +// path set to call the "api" service "AuthToken" endpoint +func (c *Client) BuildAuthTokenRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: AuthTokenAPIPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "AuthToken", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeAuthTokenRequest returns an encoder for requests sent to the api +// AuthToken server. +func EncodeAuthTokenRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*api.AuthTokenPayload) + if !ok { + return goahttp.ErrInvalidType("api", "AuthToken", "*api.AuthTokenPayload", v) + } + body := NewAuthTokenRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("api", "AuthToken", err) + } + return nil + } +} + +// DecodeAuthTokenResponse returns a decoder for responses returned by the api +// AuthToken endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeAuthTokenResponse may return the following errors: +// - "incomplete_auth_info" (type *goa.ServiceError): http.StatusUnauthorized +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "forbidden" (type *goa.ServiceError): http.StatusForbidden +// - error: internal error +func DecodeAuthTokenResponse(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 AuthTokenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + res := NewAuthTokenResultOK(&body) + return res, nil + case http.StatusUnauthorized: + en := resp.Header.Get("goa-error") + switch en { + case "incomplete_auth_info": + var ( + body AuthTokenIncompleteAuthInfoResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenIncompleteAuthInfoResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenIncompleteAuthInfo(&body) + case "unauthorized": + var ( + body AuthTokenUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "AuthToken", resp.StatusCode, string(body)) + } + case http.StatusForbidden: + var ( + body AuthTokenForbiddenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenForbiddenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenForbidden(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "AuthToken", resp.StatusCode, string(body)) + } + } +} + +// 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 +// - "forbidden" (type *goa.ServiceError): http.StatusForbidden +// - 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.StatusForbidden: + var ( + body CounterGetForbiddenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + err = ValidateCounterGetForbiddenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + return nil, NewCounterGetForbidden(&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) + } + if p.Token != nil { + head := *p.Token + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + 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: +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "forbidden" (type *goa.ServiceError): http.StatusForbidden +// - 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.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) + 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.StatusForbidden: + var ( + body CounterIncrementForbiddenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + err = ValidateCounterIncrementForbiddenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + return nil, NewCounterIncrementForbidden(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "CounterIncrement", 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..8f92ed6 --- /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 + +// AuthTokenAPIPath returns the URL path to the api service AuthToken HTTP endpoint. +func AuthTokenAPIPath() string { + return "/api/v1/auth/token" +} + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/api/v1/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/api/v1/counter" +} 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..8fc1ba6 --- /dev/null +++ b/app/api/v1/gen/http/api/client/types.go @@ -0,0 +1,558 @@ +// 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" +) + +// AuthTokenRequestBody is the type of the "api" service "AuthToken" endpoint +// HTTP request body. +type AuthTokenRequestBody struct { + Provider string `form:"provider" json:"provider" xml:"provider"` + AccessToken string `form:"access_token" json:"access_token" xml:"access_token"` +} + +// AuthTokenResponseBody is the type of the "api" service "AuthToken" endpoint +// HTTP response body. +type AuthTokenResponseBody struct { + Token *string `form:"token,omitempty" json:"token,omitempty" xml:"token,omitempty"` +} + +// 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"` +} + +// AuthTokenIncompleteAuthInfoResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "incomplete_auth_info" error. +type AuthTokenIncompleteAuthInfoResponseBody 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"` +} + +// AuthTokenUnauthorizedResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "unauthorized" error. +type AuthTokenUnauthorizedResponseBody 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"` +} + +// AuthTokenForbiddenResponseBody is the type of the "api" service "AuthToken" +// endpoint HTTP response body for the "forbidden" error. +type AuthTokenForbiddenResponseBody 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"` +} + +// 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"` +} + +// CounterGetForbiddenResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "forbidden" error. +type CounterGetForbiddenResponseBody 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"` +} + +// 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"` +} + +// CounterIncrementForbiddenResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "forbidden" error. +type CounterIncrementForbiddenResponseBody 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"` +} + +// NewAuthTokenRequestBody builds the HTTP request body from the payload of the +// "AuthToken" endpoint of the "api" service. +func NewAuthTokenRequestBody(p *api.AuthTokenPayload) *AuthTokenRequestBody { + body := &AuthTokenRequestBody{ + Provider: p.Provider, + AccessToken: p.AccessToken, + } + return body +} + +// NewAuthTokenResultOK builds a "api" service "AuthToken" endpoint result from +// a HTTP "OK" response. +func NewAuthTokenResultOK(body *AuthTokenResponseBody) *api.AuthTokenResult { + v := &api.AuthTokenResult{ + Token: *body.Token, + } + + return v +} + +// NewAuthTokenIncompleteAuthInfo builds a api service AuthToken endpoint +// incomplete_auth_info error. +func NewAuthTokenIncompleteAuthInfo(body *AuthTokenIncompleteAuthInfoResponseBody) *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 +} + +// NewAuthTokenUnauthorized builds a api service AuthToken endpoint +// unauthorized error. +func NewAuthTokenUnauthorized(body *AuthTokenUnauthorizedResponseBody) *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 +} + +// NewAuthTokenForbidden builds a api service AuthToken endpoint forbidden +// error. +func NewAuthTokenForbidden(body *AuthTokenForbiddenResponseBody) *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 +} + +// 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 +} + +// NewCounterGetForbidden builds a api service CounterGet endpoint forbidden +// error. +func NewCounterGetForbidden(body *CounterGetForbiddenResponseBody) *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 +} + +// 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 +} + +// 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 +} + +// NewCounterIncrementForbidden builds a api service CounterIncrement endpoint +// forbidden error. +func NewCounterIncrementForbidden(body *CounterIncrementForbiddenResponseBody) *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 +} + +// ValidateAuthTokenResponseBody runs the validations defined on +// AuthTokenResponseBody +func ValidateAuthTokenResponseBody(body *AuthTokenResponseBody) (err error) { + if body.Token == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "body")) + } + return +} + +// ValidateAuthTokenIncompleteAuthInfoResponseBody runs the validations defined +// on AuthToken_incomplete_auth_info_Response_Body +func ValidateAuthTokenIncompleteAuthInfoResponseBody(body *AuthTokenIncompleteAuthInfoResponseBody) (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 +} + +// ValidateAuthTokenUnauthorizedResponseBody runs the validations defined on +// AuthToken_unauthorized_Response_Body +func ValidateAuthTokenUnauthorizedResponseBody(body *AuthTokenUnauthorizedResponseBody) (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 +} + +// ValidateAuthTokenForbiddenResponseBody runs the validations defined on +// AuthToken_forbidden_Response_Body +func ValidateAuthTokenForbiddenResponseBody(body *AuthTokenForbiddenResponseBody) (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 +} + +// 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 +} + +// ValidateCounterGetForbiddenResponseBody runs the validations defined on +// CounterGet_forbidden_Response_Body +func ValidateCounterGetForbiddenResponseBody(body *CounterGetForbiddenResponseBody) (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 +} + +// 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 +} + +// ValidateCounterIncrementForbiddenResponseBody runs the validations defined +// on CounterIncrement_forbidden_Response_Body +func ValidateCounterIncrementForbiddenResponseBody(body *CounterIncrementForbiddenResponseBody) (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..20fe360 --- /dev/null +++ b/app/api/v1/gen/http/api/server/encode_decode.go @@ -0,0 +1,262 @@ +// 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" + "strings" + + 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" +) + +// EncodeAuthTokenResponse returns an encoder for responses returned by the api +// AuthToken endpoint. +func EncodeAuthTokenResponse(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.AuthTokenResult) + enc := encoder(ctx, w) + body := NewAuthTokenResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeAuthTokenRequest returns a decoder for requests sent to the api +// AuthToken endpoint. +func DecodeAuthTokenRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body AuthTokenRequestBody + 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 = ValidateAuthTokenRequestBody(&body) + if err != nil { + return nil, err + } + payload := NewAuthTokenPayload(&body) + + return payload, nil + } +} + +// EncodeAuthTokenError returns an encoder for errors returned by the AuthToken +// api endpoint. +func EncodeAuthTokenError(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 "incomplete_auth_info": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAuthTokenIncompleteAuthInfoResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + 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 = NewAuthTokenUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "forbidden": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAuthTokenForbiddenResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusForbidden) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// 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 "forbidden": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterGetForbiddenResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusForbidden) + 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 ( + token *string + ) + tokenRaw := r.Header.Get("Authorization") + if tokenRaw != "" { + token = &tokenRaw + } + payload := NewCounterIncrementPayload(token) + if payload.Token != nil { + if strings.Contains(*payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.Token, " ", 2)[1] + payload.Token = &cred + } + } + + 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 "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) + 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 "forbidden": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterIncrementForbiddenResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusForbidden) + 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..9bf4fd6 --- /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 + +// AuthTokenAPIPath returns the URL path to the api service AuthToken HTTP endpoint. +func AuthTokenAPIPath() string { + return "/api/v1/auth/token" +} + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/api/v1/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/api/v1/counter" +} 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..9a4a1e1 --- /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 + AuthToken http.Handler + CounterGet http.Handler + CounterIncrement 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{ + {"AuthToken", "POST", "/api/v1/auth/token"}, + {"CounterGet", "GET", "/api/v1/counter"}, + {"CounterIncrement", "POST", "/api/v1/counter"}, + {"Serve gen/http/openapi3.json", "GET", "/api/v1/openapi.json"}, + }, + AuthToken: NewAuthTokenHandler(e.AuthToken, mux, decoder, encoder, errhandler, formatter), + CounterGet: NewCounterGetHandler(e.CounterGet, mux, decoder, encoder, errhandler, formatter), + CounterIncrement: NewCounterIncrementHandler(e.CounterIncrement, 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.AuthToken = m(s.AuthToken) + s.CounterGet = m(s.CounterGet) + s.CounterIncrement = m(s.CounterIncrement) +} + +// 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) { + MountAuthTokenHandler(mux, h.AuthToken) + MountCounterGetHandler(mux, h.CounterGet) + MountCounterIncrementHandler(mux, h.CounterIncrement) + MountGenHTTPOpenapi3JSON(mux, http.StripPrefix("/api/v1", h.GenHTTPOpenapi3JSON)) +} + +// Mount configures the mux to serve the api endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountAuthTokenHandler configures the mux to serve the "api" service +// "AuthToken" endpoint. +func MountAuthTokenHandler(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", "/api/v1/auth/token", f) +} + +// NewAuthTokenHandler creates a HTTP handler which loads the HTTP request and +// calls the "api" service "AuthToken" endpoint. +func NewAuthTokenHandler( + 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 = DecodeAuthTokenRequest(mux, decoder) + encodeResponse = EncodeAuthTokenResponse(encoder) + encodeError = EncodeAuthTokenError(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, "AuthToken") + 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) + } + }) +} + +// 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", "/api/v1/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", "/api/v1/counter", 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) + } + }) +} + +// 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 +// "/api/v1/openapi.json". +func MountGenHTTPOpenapi3JSON(mux goahttp.Muxer, h http.Handler) { + mux.Handle("GET", "/api/v1/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..0948f9e --- /dev/null +++ b/app/api/v1/gen/http/api/server/types.go @@ -0,0 +1,372 @@ +// 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" +) + +// AuthTokenRequestBody is the type of the "api" service "AuthToken" endpoint +// HTTP request body. +type AuthTokenRequestBody struct { + Provider *string `form:"provider,omitempty" json:"provider,omitempty" xml:"provider,omitempty"` + AccessToken *string `form:"access_token,omitempty" json:"access_token,omitempty" xml:"access_token,omitempty"` +} + +// AuthTokenResponseBody is the type of the "api" service "AuthToken" endpoint +// HTTP response body. +type AuthTokenResponseBody struct { + Token string `form:"token" json:"token" xml:"token"` +} + +// 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"` +} + +// AuthTokenIncompleteAuthInfoResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "incomplete_auth_info" error. +type AuthTokenIncompleteAuthInfoResponseBody 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"` +} + +// AuthTokenUnauthorizedResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "unauthorized" error. +type AuthTokenUnauthorizedResponseBody 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"` +} + +// AuthTokenForbiddenResponseBody is the type of the "api" service "AuthToken" +// endpoint HTTP response body for the "forbidden" error. +type AuthTokenForbiddenResponseBody 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"` +} + +// 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"` +} + +// CounterGetForbiddenResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "forbidden" error. +type CounterGetForbiddenResponseBody 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"` +} + +// 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"` +} + +// CounterIncrementForbiddenResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "forbidden" error. +type CounterIncrementForbiddenResponseBody 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"` +} + +// NewAuthTokenResponseBody builds the HTTP response body from the result of +// the "AuthToken" endpoint of the "api" service. +func NewAuthTokenResponseBody(res *api.AuthTokenResult) *AuthTokenResponseBody { + body := &AuthTokenResponseBody{ + Token: res.Token, + } + return body +} + +// 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 +} + +// NewAuthTokenIncompleteAuthInfoResponseBody builds the HTTP response body +// from the result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenIncompleteAuthInfoResponseBody(res *goa.ServiceError) *AuthTokenIncompleteAuthInfoResponseBody { + body := &AuthTokenIncompleteAuthInfoResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAuthTokenUnauthorizedResponseBody builds the HTTP response body from the +// result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenUnauthorizedResponseBody(res *goa.ServiceError) *AuthTokenUnauthorizedResponseBody { + body := &AuthTokenUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAuthTokenForbiddenResponseBody builds the HTTP response body from the +// result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenForbiddenResponseBody(res *goa.ServiceError) *AuthTokenForbiddenResponseBody { + body := &AuthTokenForbiddenResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + 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 +} + +// NewCounterGetForbiddenResponseBody builds the HTTP response body from the +// result of the "CounterGet" endpoint of the "api" service. +func NewCounterGetForbiddenResponseBody(res *goa.ServiceError) *CounterGetForbiddenResponseBody { + body := &CounterGetForbiddenResponseBody{ + 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 +} + +// 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 +} + +// NewCounterIncrementForbiddenResponseBody builds the HTTP response body from +// the result of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementForbiddenResponseBody(res *goa.ServiceError) *CounterIncrementForbiddenResponseBody { + body := &CounterIncrementForbiddenResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAuthTokenPayload builds a api service AuthToken endpoint payload. +func NewAuthTokenPayload(body *AuthTokenRequestBody) *api.AuthTokenPayload { + v := &api.AuthTokenPayload{ + Provider: *body.Provider, + AccessToken: *body.AccessToken, + } + + return v +} + +// NewCounterIncrementPayload builds a api service CounterIncrement endpoint +// payload. +func NewCounterIncrementPayload(token *string) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{} + v.Token = token + + return v +} + +// ValidateAuthTokenRequestBody runs the validations defined on +// AuthTokenRequestBody +func ValidateAuthTokenRequestBody(body *AuthTokenRequestBody) (err error) { + if body.Provider == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("provider", "body")) + } + if body.AccessToken == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("access_token", "body")) + } + if body.Provider != nil { + if !(*body.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("body.provider", *body.Provider, []any{"google"})) + } + } + 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..1e49cff --- /dev/null +++ b/app/api/v1/gen/http/cli/countup/cli.go @@ -0,0 +1,408 @@ +// 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" + teapotc "github.com/jace-ys/countup/api/v1/gen/http/teapot/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 (auth-token|counter-get|counter-increment) +web (index|another|login-google|login-google-callback|logout|session-token) +teapot echo +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api auth-token --body '{ + "access_token": "Itaque unde qui ut molestiae et omnis.", + "provider": "google" + }'` + "\n" + + os.Args[0] + ` web index` + "\n" + + os.Args[0] + ` teapot echo --body '{ + "text": "Eos itaque." + }'` + "\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) + + apiAuthTokenFlags = flag.NewFlagSet("auth-token", flag.ExitOnError) + apiAuthTokenBodyFlag = apiAuthTokenFlags.String("body", "REQUIRED", "") + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementTokenFlag = apiCounterIncrementFlags.String("token", "", "") + + webFlags = flag.NewFlagSet("web", flag.ContinueOnError) + + webIndexFlags = flag.NewFlagSet("index", flag.ExitOnError) + + webAnotherFlags = flag.NewFlagSet("another", flag.ExitOnError) + + webLoginGoogleFlags = flag.NewFlagSet("login-google", flag.ExitOnError) + + webLoginGoogleCallbackFlags = flag.NewFlagSet("login-google-callback", flag.ExitOnError) + webLoginGoogleCallbackCodeFlag = webLoginGoogleCallbackFlags.String("code", "REQUIRED", "") + webLoginGoogleCallbackStateFlag = webLoginGoogleCallbackFlags.String("state", "REQUIRED", "") + webLoginGoogleCallbackSessionCookieFlag = webLoginGoogleCallbackFlags.String("session-cookie", "REQUIRED", "") + + webLogoutFlags = flag.NewFlagSet("logout", flag.ExitOnError) + webLogoutSessionCookieFlag = webLogoutFlags.String("session-cookie", "REQUIRED", "") + + webSessionTokenFlags = flag.NewFlagSet("session-token", flag.ExitOnError) + webSessionTokenSessionCookieFlag = webSessionTokenFlags.String("session-cookie", "REQUIRED", "") + + teapotFlags = flag.NewFlagSet("teapot", flag.ContinueOnError) + + teapotEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + teapotEchoBodyFlag = teapotEchoFlags.String("body", "REQUIRED", "") + ) + apiFlags.Usage = apiUsage + apiAuthTokenFlags.Usage = apiAuthTokenUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + + webFlags.Usage = webUsage + webIndexFlags.Usage = webIndexUsage + webAnotherFlags.Usage = webAnotherUsage + webLoginGoogleFlags.Usage = webLoginGoogleUsage + webLoginGoogleCallbackFlags.Usage = webLoginGoogleCallbackUsage + webLogoutFlags.Usage = webLogoutUsage + webSessionTokenFlags.Usage = webSessionTokenUsage + + teapotFlags.Usage = teapotUsage + teapotEchoFlags.Usage = teapotEchoUsage + + 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 + case "teapot": + svcf = teapotFlags + 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 "auth-token": + epf = apiAuthTokenFlags + + case "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + } + + case "web": + switch epn { + case "index": + epf = webIndexFlags + + case "another": + epf = webAnotherFlags + + case "login-google": + epf = webLoginGoogleFlags + + case "login-google-callback": + epf = webLoginGoogleCallbackFlags + + case "logout": + epf = webLogoutFlags + + case "session-token": + epf = webSessionTokenFlags + + } + + case "teapot": + switch epn { + case "echo": + epf = teapotEchoFlags + + } + + } + } + 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 "auth-token": + endpoint = c.AuthToken() + data, err = apic.BuildAuthTokenPayload(*apiAuthTokenBodyFlag) + case "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementTokenFlag) + } + case "web": + c := webc.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "index": + endpoint = c.Index() + case "another": + endpoint = c.Another() + case "login-google": + endpoint = c.LoginGoogle() + case "login-google-callback": + endpoint = c.LoginGoogleCallback() + data, err = webc.BuildLoginGoogleCallbackPayload(*webLoginGoogleCallbackCodeFlag, *webLoginGoogleCallbackStateFlag, *webLoginGoogleCallbackSessionCookieFlag) + case "logout": + endpoint = c.Logout() + data, err = webc.BuildLogoutPayload(*webLogoutSessionCookieFlag) + case "session-token": + endpoint = c.SessionToken() + data, err = webc.BuildSessionTokenPayload(*webSessionTokenSessionCookieFlag) + } + case "teapot": + c := teapotc.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "echo": + endpoint = c.Echo() + data, err = teapotc.BuildEchoPayload(*teapotEchoBodyFlag) + } + } + } + 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: + auth-token: AuthToken implements AuthToken. + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + +Additional help: + %[1]s api COMMAND --help +`, os.Args[0]) +} +func apiAuthTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api auth-token -body JSON + +AuthToken implements AuthToken. + -body JSON: + +Example: + %[1]s api auth-token --body '{ + "access_token": "Itaque unde qui ut molestiae et omnis.", + "provider": "google" + }' +`, 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 -token STRING + +CounterIncrement implements CounterIncrement. + -token STRING: + +Example: + %[1]s api counter-increment --token "Sapiente beatae ullam itaque fugit qui." +`, 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. + login-google: LoginGoogle implements LoginGoogle. + login-google-callback: LoginGoogleCallback implements LoginGoogleCallback. + logout: Logout implements Logout. + session-token: SessionToken implements SessionToken. + +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]) +} + +func webLoginGoogleUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web login-google + +LoginGoogle implements LoginGoogle. + +Example: + %[1]s web login-google +`, os.Args[0]) +} + +func webLoginGoogleCallbackUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web login-google-callback -code STRING -state STRING -session-cookie STRING + +LoginGoogleCallback implements LoginGoogleCallback. + -code STRING: + -state STRING: + -session-cookie STRING: + +Example: + %[1]s web login-google-callback --code "Blanditiis non consectetur." --state "Laboriosam quasi." --session-cookie "Natus minima non perspiciatis eum dicta." +`, os.Args[0]) +} + +func webLogoutUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web logout -session-cookie STRING + +Logout implements Logout. + -session-cookie STRING: + +Example: + %[1]s web logout --session-cookie "Eum dolor alias sequi dolore." +`, os.Args[0]) +} + +func webSessionTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web session-token -session-cookie STRING + +SessionToken implements SessionToken. + -session-cookie STRING: + +Example: + %[1]s web session-token --session-cookie "Aliquam quia nemo voluptas." +`, os.Args[0]) +} + +// teapotUsage displays the usage of the teapot command and its subcommands. +func teapotUsage() { + fmt.Fprintf(os.Stderr, `Service is the teapot service interface. +Usage: + %[1]s [globalflags] teapot COMMAND [flags] + +COMMAND: + echo: Echo implements Echo. + +Additional help: + %[1]s teapot COMMAND --help +`, os.Args[0]) +} +func teapotEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] teapot echo -body JSON + +Echo implements Echo. + -body JSON: + +Example: + %[1]s teapot echo --body '{ + "text": "Eos itaque." + }' +`, 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..5f7df50 --- /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"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebIndexUnauthorizedResponseBody"}}},"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"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebAnotherUnauthorizedResponseBody"}}},"schemes":["http"]}},"/api/v1/auth/token":{"post":{"tags":["api"],"summary":"AuthToken api","operationId":"api#AuthToken","parameters":[{"name":"AuthTokenRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APIAuthTokenRequestBody","required":["provider","access_token"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/APIAuthTokenResponseBody","required":["token"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APIAuthTokenUnauthorizedResponseBody"}},"403":{"description":"Forbidden response.","schema":{"$ref":"#/definitions/APIAuthTokenForbiddenResponseBody"}}},"schemes":["http"]}},"/api/v1/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"}},"403":{"description":"Forbidden response.","schema":{"$ref":"#/definitions/APICounterGetForbiddenResponseBody"}}},"schemes":["http"]},"post":{"tags":["api"],"summary":"CounterIncrement api","description":"\n**Required security scopes for jwt**:\n * `api.user`","operationId":"api#CounterIncrement","parameters":[{"name":"Authorization","in":"header","required":false,"type":"string"}],"responses":{"202":{"description":"Accepted response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterIncrementUnauthorizedResponseBody"}},"403":{"description":"Forbidden response.","schema":{"$ref":"#/definitions/APICounterIncrementForbiddenResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterIncrementExistingIncrementRequestResponseBody"}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/api/v1/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/api/v1/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/echo":{"post":{"tags":["teapot"],"summary":"Echo teapot","operationId":"teapot#Echo","parameters":[{"name":"EchoRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/TeapotEchoRequestBody","required":["text"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TeapotEchoResponseBody","required":["text"]}}},"schemes":["http"]}},"/login/google":{"get":{"tags":["web"],"summary":"LoginGoogle web","operationId":"web#LoginGoogle","responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLoginGoogleUnauthorizedResponseBody"}}},"schemes":["http"]}},"/login/google/callback":{"get":{"tags":["web"],"summary":"LoginGoogleCallback web","operationId":"web#LoginGoogleCallback","parameters":[{"name":"code","in":"query","required":true,"type":"string"},{"name":"state","in":"query","required":true,"type":"string"}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLoginGoogleCallbackUnauthorizedResponseBody"}}},"schemes":["http"]}},"/logout":{"get":{"tags":["web"],"summary":"Logout web","operationId":"web#Logout","responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLogoutUnauthorizedResponseBody"}}},"schemes":["http"]}},"/openapi.json":{"get":{"tags":["teapot"],"summary":"Download gen/http/openapi3.json","operationId":"teapot#/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/session/token":{"get":{"tags":["web"],"summary":"SessionToken web","operationId":"web#SessionToken","produces":["application/json"],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/WebSessionTokenResponseBody","required":["token"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebSessionTokenUnauthorizedResponseBody"}}},"schemes":["http"]}},"/static/*":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/*","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}}},"definitions":{"APIAuthTokenForbiddenResponseBody":{"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":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"AuthToken_forbidden_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APIAuthTokenIncompleteAuthInfoResponseBody":{"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":false}},"description":"AuthToken_incomplete_auth_info_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APIAuthTokenRequestBody":{"title":"APIAuthTokenRequestBody","type":"object","properties":{"access_token":{"type":"string","example":"Aut culpa quo sit dolor aperiam consequatur."},"provider":{"type":"string","example":"google","enum":["google"]}},"example":{"access_token":"Harum iusto quas.","provider":"google"},"required":["provider","access_token"]},"APIAuthTokenResponseBody":{"title":"APIAuthTokenResponseBody","type":"object","properties":{"token":{"type":"string","example":"Error animi atque nobis sit dolor ut."}},"example":{"token":"Illo vel qui ipsa adipisci."},"required":["token"]},"APIAuthTokenUnauthorizedResponseBody":{"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":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"AuthToken_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterGetForbiddenResponseBody":{"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":"CounterGet_forbidden_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"]},"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":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_unauthorized_Response_Body 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"]},"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":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementForbiddenResponseBody":{"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":"CounterIncrement_forbidden_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"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":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_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":718974130,"format":"int32"},"last_increment_at":{"type":"string","example":"Consequuntur cupiditate pariatur aut placeat."},"last_increment_by":{"type":"string","example":"Ipsam nesciunt minima cupiditate."},"next_finalize_at":{"type":"string","example":"Amet voluptas ex nostrum aperiam explicabo sed."}},"description":"CounterGetResponseBody result type (default view)","example":{"count":837784502,"last_increment_at":"Eius rerum.","last_increment_by":"Sint asperiores sed voluptas voluptatem.","next_finalize_at":"Accusamus maiores ut voluptas."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"TeapotEchoRequestBody":{"title":"TeapotEchoRequestBody","type":"object","properties":{"text":{"type":"string","example":"Sit non qui quaerat nobis incidunt porro."}},"example":{"text":"Quam ut nobis reiciendis."},"required":["text"]},"TeapotEchoResponseBody":{"title":"TeapotEchoResponseBody","type":"object","properties":{"text":{"type":"string","example":"Aliquid dignissimos non at doloremque."}},"example":{"text":"Et optio hic."},"required":["text"]},"WebAnotherUnauthorizedResponseBody":{"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":"Another_unauthorized_Response_Body 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"]},"WebIndexUnauthorizedResponseBody":{"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":false}},"description":"Index_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebLoginGoogleCallbackUnauthorizedResponseBody":{"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":"LoginGoogleCallback_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"]},"WebLoginGoogleUnauthorizedResponseBody":{"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":"LoginGoogle_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"]},"WebLogoutUnauthorizedResponseBody":{"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":"Logout_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebSessionTokenResponseBody":{"title":"WebSessionTokenResponseBody","type":"object","properties":{"token":{"type":"string","example":"Ut aut possimus et."}},"example":{"token":"Magni harum atque dolor quod at consectetur."},"required":["token"]},"WebSessionTokenUnauthorizedResponseBody":{"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":"SessionToken_unauthorized_Response_Body result type (default view)","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"]}},"securityDefinitions":{"jwt_header_Authorization":{"type":"apiKey","description":"\n**Security Scopes**:\n * `api.user`: no description","name":"Authorization","in":"header"}}} \ 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..f5bc45f --- /dev/null +++ b/app/api/v1/gen/http/openapi.yaml @@ -0,0 +1,989 @@ +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 + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebIndexUnauthorizedResponseBody' + 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 + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebAnotherUnauthorizedResponseBody' + schemes: + - http + /api/v1/auth/token: + post: + tags: + - api + summary: AuthToken api + operationId: api#AuthToken + parameters: + - name: AuthTokenRequestBody + in: body + required: true + schema: + $ref: '#/definitions/APIAuthTokenRequestBody' + required: + - provider + - access_token + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/APIAuthTokenResponseBody' + required: + - token + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APIAuthTokenUnauthorizedResponseBody' + "403": + description: Forbidden response. + schema: + $ref: '#/definitions/APIAuthTokenForbiddenResponseBody' + schemes: + - http + /api/v1/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' + "403": + description: Forbidden response. + schema: + $ref: '#/definitions/APICounterGetForbiddenResponseBody' + schemes: + - http + post: + tags: + - api + summary: CounterIncrement api + description: |4- + **Required security scopes for jwt**: + * `api.user` + operationId: api#CounterIncrement + parameters: + - name: Authorization + in: header + required: false + type: string + responses: + "202": + description: Accepted response. + schema: + $ref: '#/definitions/CounterInfo' + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APICounterIncrementUnauthorizedResponseBody' + "403": + description: Forbidden response. + schema: + $ref: '#/definitions/APICounterIncrementForbiddenResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APICounterIncrementExistingIncrementRequestResponseBody' + schemes: + - http + security: + - jwt_header_Authorization: [] + /api/v1/openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/api/v1/openapi.json + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http + /echo: + post: + tags: + - teapot + summary: Echo teapot + operationId: teapot#Echo + parameters: + - name: EchoRequestBody + in: body + required: true + schema: + $ref: '#/definitions/TeapotEchoRequestBody' + required: + - text + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/TeapotEchoResponseBody' + required: + - text + schemes: + - http + /login/google: + get: + tags: + - web + summary: LoginGoogle web + operationId: web#LoginGoogle + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLoginGoogleUnauthorizedResponseBody' + schemes: + - http + /login/google/callback: + get: + tags: + - web + summary: LoginGoogleCallback web + operationId: web#LoginGoogleCallback + parameters: + - name: code + in: query + required: true + type: string + - name: state + in: query + required: true + type: string + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLoginGoogleCallbackUnauthorizedResponseBody' + schemes: + - http + /logout: + get: + tags: + - web + summary: Logout web + operationId: web#Logout + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLogoutUnauthorizedResponseBody' + schemes: + - http + /openapi.json: + get: + tags: + - teapot + summary: Download gen/http/openapi3.json + operationId: teapot#/openapi.json + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http + /session/token: + get: + tags: + - web + summary: SessionToken web + operationId: web#SessionToken + produces: + - application/json + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/WebSessionTokenResponseBody' + required: + - token + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebSessionTokenUnauthorizedResponseBody' + schemes: + - http + /static/*: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/* + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http +definitions: + APIAuthTokenForbiddenResponseBody: + 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: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: AuthToken_forbidden_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APIAuthTokenIncompleteAuthInfoResponseBody: + 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: false + description: AuthToken_incomplete_auth_info_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APIAuthTokenRequestBody: + title: APIAuthTokenRequestBody + type: object + properties: + access_token: + type: string + example: Aut culpa quo sit dolor aperiam consequatur. + provider: + type: string + example: google + enum: + - google + example: + access_token: Harum iusto quas. + provider: google + required: + - provider + - access_token + APIAuthTokenResponseBody: + title: APIAuthTokenResponseBody + type: object + properties: + token: + type: string + example: Error animi atque nobis sit dolor ut. + example: + token: Illo vel qui ipsa adipisci. + required: + - token + APIAuthTokenUnauthorizedResponseBody: + 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: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: AuthToken_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterGetForbiddenResponseBody: + 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: CounterGet_forbidden_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 + 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: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterGet_unauthorized_Response_Body 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 + 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: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterIncrementForbiddenResponseBody: + 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: CounterIncrement_forbidden_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + 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: 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_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: 718974130 + format: int32 + last_increment_at: + type: string + example: Consequuntur cupiditate pariatur aut placeat. + last_increment_by: + type: string + example: Ipsam nesciunt minima cupiditate. + next_finalize_at: + type: string + example: Amet voluptas ex nostrum aperiam explicabo sed. + description: CounterGetResponseBody result type (default view) + example: + count: 837784502 + last_increment_at: Eius rerum. + last_increment_by: Sint asperiores sed voluptas voluptatem. + next_finalize_at: Accusamus maiores ut voluptas. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + TeapotEchoRequestBody: + title: TeapotEchoRequestBody + type: object + properties: + text: + type: string + example: Sit non qui quaerat nobis incidunt porro. + example: + text: Quam ut nobis reiciendis. + required: + - text + TeapotEchoResponseBody: + title: TeapotEchoResponseBody + type: object + properties: + text: + type: string + example: Aliquid dignissimos non at doloremque. + example: + text: Et optio hic. + required: + - text + WebAnotherUnauthorizedResponseBody: + 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: Another_unauthorized_Response_Body 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 + WebIndexUnauthorizedResponseBody: + 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: false + description: Index_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebLoginGoogleCallbackUnauthorizedResponseBody: + 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: LoginGoogleCallback_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 + WebLoginGoogleUnauthorizedResponseBody: + 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: LoginGoogle_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 + WebLogoutUnauthorizedResponseBody: + 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: Logout_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebSessionTokenResponseBody: + title: WebSessionTokenResponseBody + type: object + properties: + token: + type: string + example: Ut aut possimus et. + example: + token: Magni harum atque dolor quod at consectetur. + required: + - token + WebSessionTokenUnauthorizedResponseBody: + 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: SessionToken_unauthorized_Response_Body result type (default view) + 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 +securityDefinitions: + jwt_header_Authorization: + type: apiKey + description: |4- + **Security Scopes**: + * `api.user`: no description + name: Authorization + in: header diff --git a/app/api/v1/gen/http/openapi3.json b/app/api/v1/gen/http/openapi3.json new file mode 100644 index 0000000..dedd996 --- /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":"SWxsbyBjdW0u","format":"binary"},"example":"RG9sb3JpYnVzIHZvbHVwdGF0ZW0gaGljIHRvdGFtIHJlcHVkaWFuZGFlLg=="}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/another":{"get":{"tags":["web"],"summary":"Another web","operationId":"web#Another","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"RXN0IGFzc3VtZW5kYSBlbmltIGVhIGFzcGVybmF0dXIgdXQu","format":"binary"},"example":"RW5pbSBhbGlhcyBzdW50Lg=="}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/token":{"post":{"tags":["api"],"summary":"AuthToken api","operationId":"api#AuthToken","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenRequestBody"},"example":{"access_token":"Itaque unde qui ut molestiae et omnis.","provider":"google"}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponseBody"},"example":{"token":"Est asperiores."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"forbidden: Forbidden response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/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":855809606,"last_increment_at":"Non accusantium eos culpa autem illum architecto.","last_increment_by":"Eum inventore.","next_finalize_at":"Vel enim ut autem quo."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"forbidden: Forbidden response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","responses":{"202":{"description":"Accepted response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":1227174102,"last_increment_at":"Harum error iste ipsam.","last_increment_by":"Quae cupiditate.","next_finalize_at":"Nobis non est."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"forbidden: Forbidden 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"}}}}},"security":[{"jwt_header_Authorization":["api.user"]}]}},"/api/v1/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/api/v1/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/echo":{"post":{"tags":["teapot"],"summary":"Echo teapot","operationId":"teapot#Echo","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Eos itaque."}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Et veniam et illum quaerat et et."}}}}}}},"/login/google":{"get":{"tags":["web"],"summary":"LoginGoogle web","operationId":"web#LoginGoogle","responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Dolores velit."},"example":"Repellendus quo error."},"Set-Cookie":{"schema":{"type":"string","example":"Optio et."},"example":"Mollitia similique dignissimos."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/login/google/callback":{"get":{"tags":["web"],"summary":"LoginGoogleCallback web","operationId":"web#LoginGoogleCallback","parameters":[{"name":"code","in":"query","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Libero soluta."},"example":"Exercitationem omnis perferendis ipsa dolor eum."},{"name":"state","in":"query","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Consequuntur quo excepturi eos dolor voluptatem."},"example":"Quia et nulla."},{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Explicabo veritatis labore quidem deserunt enim qui."},"example":"Aut ab."}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Officiis consequuntur mollitia provident recusandae."},"example":"Expedita sint inventore possimus cumque magni."},"Set-Cookie":{"schema":{"type":"string","example":"Deleniti facere eius eos."},"example":"Omnis distinctio adipisci."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/logout":{"get":{"tags":["web"],"summary":"Logout web","operationId":"web#Logout","parameters":[{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Aut magni consequatur omnis."},"example":"Soluta necessitatibus ad modi esse."}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Iusto architecto corporis commodi aut minus."},"example":"Quo aut."},"Set-Cookie":{"schema":{"type":"string","example":"Illo qui sit tenetur accusamus tempore laboriosam."},"example":"Ea voluptas sit consectetur alias libero saepe."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"tags":["teapot"],"summary":"Download gen/http/openapi3.json","operationId":"teapot#/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/session/token":{"get":{"tags":["web"],"summary":"SessionToken web","operationId":"web#SessionToken","parameters":[{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Ipsam autem."},"example":"Error earum ex."}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponseBody"},"example":{"token":"Et consectetur."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/static/*":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/*","responses":{"200":{"description":"File downloaded"}}}}},"components":{"schemas":{"AuthTokenRequestBody":{"type":"object","properties":{"access_token":{"type":"string","example":"Velit ullam itaque temporibus eius voluptatem aliquam."},"provider":{"type":"string","example":"google","enum":["google"]}},"example":{"access_token":"Pariatur dolores unde.","provider":"google"},"required":["provider","access_token"]},"AuthTokenResponseBody":{"type":"object","properties":{"token":{"type":"string","example":"Et sint velit aut blanditiis eius."}},"example":{"token":"Fugiat quia facilis sint eligendi commodi sit."},"required":["token"]},"CounterInfo":{"type":"object","properties":{"count":{"type":"integer","example":2045787089,"format":"int32"},"last_increment_at":{"type":"string","example":"Optio aut est."},"last_increment_by":{"type":"string","example":"Ex eos tempora eum."},"next_finalize_at":{"type":"string","example":"Animi rerum aut adipisci."}},"example":{"count":116253852,"last_increment_at":"Sunt ex distinctio et rerum pariatur.","last_increment_by":"Iusto perspiciatis laborum distinctio rem ipsam rem.","next_finalize_at":"Nihil totam ipsum."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"EchoRequestBody":{"type":"object","properties":{"text":{"type":"string","example":"Illo sequi."}},"example":{"text":"Aut dolorem."},"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":true}},"example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]}},"securitySchemes":{"jwt_header_Authorization":{"type":"http","scheme":"bearer"}}},"tags":[{"name":"api"},{"name":"web"},{"name":"teapot"}]} \ 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..68065ae --- /dev/null +++ b/app/api/v1/gen/http/openapi3.yaml @@ -0,0 +1,567 @@ +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: + - 73 + - 108 + - 108 + - 111 + - 32 + - 99 + - 117 + - 109 + - 46 + format: binary + example: + - 68 + - 111 + - 108 + - 111 + - 114 + - 105 + - 98 + - 117 + - 115 + - 32 + - 118 + - 111 + - 108 + - 117 + - 112 + - 116 + - 97 + - 116 + - 101 + - 109 + - 32 + - 104 + - 105 + - 99 + - 32 + - 116 + - 111 + - 116 + - 97 + - 109 + - 32 + - 114 + - 101 + - 112 + - 117 + - 100 + - 105 + - 97 + - 110 + - 100 + - 97 + - 101 + - 46 + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /another: + get: + tags: + - web + summary: Another web + operationId: web#Another + responses: + "200": + description: OK response. + content: + text/html: + schema: + type: string + example: + - 69 + - 115 + - 116 + - 32 + - 97 + - 115 + - 115 + - 117 + - 109 + - 101 + - 110 + - 100 + - 97 + - 32 + - 101 + - 110 + - 105 + - 109 + - 32 + - 101 + - 97 + - 32 + - 97 + - 115 + - 112 + - 101 + - 114 + - 110 + - 97 + - 116 + - 117 + - 114 + - 32 + - 117 + - 116 + - 46 + format: binary + example: + - 69 + - 110 + - 105 + - 109 + - 32 + - 97 + - 108 + - 105 + - 97 + - 115 + - 32 + - 115 + - 117 + - 110 + - 116 + - 46 + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /api/v1/auth/token: + post: + tags: + - api + summary: AuthToken api + operationId: api#AuthToken + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenRequestBody' + example: + access_token: Itaque unde qui ut molestiae et omnis. + provider: google + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponseBody' + example: + token: Est asperiores. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "403": + description: 'forbidden: Forbidden response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /api/v1/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: 855809606 + last_increment_at: Non accusantium eos culpa autem illum architecto. + last_increment_by: Eum inventore. + next_finalize_at: Vel enim ut autem quo. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "403": + description: 'forbidden: Forbidden response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - api + summary: CounterIncrement api + operationId: api#CounterIncrement + responses: + "202": + description: Accepted response. + content: + application/json: + schema: + $ref: '#/components/schemas/CounterInfo' + example: + count: 1227174102 + last_increment_at: Harum error iste ipsam. + last_increment_by: Quae cupiditate. + next_finalize_at: Nobis non est. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "403": + description: 'forbidden: Forbidden 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' + security: + - jwt_header_Authorization: + - api.user + /api/v1/openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/api/v1/openapi.json + responses: + "200": + description: File downloaded + /echo: + post: + tags: + - teapot + summary: Echo teapot + operationId: teapot#Echo + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Eos itaque. + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Et veniam et illum quaerat et et. + /login/google: + get: + tags: + - web + summary: LoginGoogle web + operationId: web#LoginGoogle + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Dolores velit. + example: Repellendus quo error. + Set-Cookie: + schema: + type: string + example: Optio et. + example: Mollitia similique dignissimos. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /login/google/callback: + get: + tags: + - web + summary: LoginGoogleCallback web + operationId: web#LoginGoogleCallback + parameters: + - name: code + in: query + allowEmptyValue: true + required: true + schema: + type: string + example: Libero soluta. + example: Exercitationem omnis perferendis ipsa dolor eum. + - name: state + in: query + allowEmptyValue: true + required: true + schema: + type: string + example: Consequuntur quo excepturi eos dolor voluptatem. + example: Quia et nulla. + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Explicabo veritatis labore quidem deserunt enim qui. + example: Aut ab. + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Officiis consequuntur mollitia provident recusandae. + example: Expedita sint inventore possimus cumque magni. + Set-Cookie: + schema: + type: string + example: Deleniti facere eius eos. + example: Omnis distinctio adipisci. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /logout: + get: + tags: + - web + summary: Logout web + operationId: web#Logout + parameters: + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Aut magni consequatur omnis. + example: Soluta necessitatibus ad modi esse. + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Iusto architecto corporis commodi aut minus. + example: Quo aut. + Set-Cookie: + schema: + type: string + example: Illo qui sit tenetur accusamus tempore laboriosam. + example: Ea voluptas sit consectetur alias libero saepe. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /openapi.json: + get: + tags: + - teapot + summary: Download gen/http/openapi3.json + operationId: teapot#/openapi.json + responses: + "200": + description: File downloaded + /session/token: + get: + tags: + - web + summary: SessionToken web + operationId: web#SessionToken + parameters: + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Ipsam autem. + example: Error earum ex. + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponseBody' + example: + token: Et consectetur. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /static/*: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/* + responses: + "200": + description: File downloaded +components: + schemas: + AuthTokenRequestBody: + type: object + properties: + access_token: + type: string + example: Velit ullam itaque temporibus eius voluptatem aliquam. + provider: + type: string + example: google + enum: + - google + example: + access_token: Pariatur dolores unde. + provider: google + required: + - provider + - access_token + AuthTokenResponseBody: + type: object + properties: + token: + type: string + example: Et sint velit aut blanditiis eius. + example: + token: Fugiat quia facilis sint eligendi commodi sit. + required: + - token + CounterInfo: + type: object + properties: + count: + type: integer + example: 2045787089 + format: int32 + last_increment_at: + type: string + example: Optio aut est. + last_increment_by: + type: string + example: Ex eos tempora eum. + next_finalize_at: + type: string + example: Animi rerum aut adipisci. + example: + count: 116253852 + last_increment_at: Sunt ex distinctio et rerum pariatur. + last_increment_by: Iusto perspiciatis laborum distinctio rem ipsam rem. + next_finalize_at: Nihil totam ipsum. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + EchoRequestBody: + type: object + properties: + text: + type: string + example: Illo sequi. + example: + text: Aut dolorem. + 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: true + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + securitySchemes: + jwt_header_Authorization: + type: http + scheme: bearer +tags: + - name: api + - name: web + - name: teapot diff --git a/app/api/v1/gen/http/teapot/client/cli.go b/app/api/v1/gen/http/teapot/client/cli.go new file mode 100644 index 0000000..62d8dfa --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/cli.go @@ -0,0 +1,33 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// BuildEchoPayload builds the payload for the teapot Echo endpoint from CLI +// flags. +func BuildEchoPayload(teapotEchoBody string) (*teapot.EchoPayload, error) { + var err error + var body EchoRequestBody + { + err = json.Unmarshal([]byte(teapotEchoBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Eos itaque.\"\n }'") + } + } + v := &teapot.EchoPayload{ + Text: body.Text, + } + + return v, nil +} diff --git a/app/api/v1/gen/http/teapot/client/client.go b/app/api/v1/gen/http/teapot/client/client.go new file mode 100644 index 0000000..3e791e2 --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/client.go @@ -0,0 +1,74 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot 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 teapot service endpoint HTTP clients. +type Client struct { + // 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 teapot 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{ + EchoDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// Echo returns an endpoint that makes HTTP requests to the teapot 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("teapot", "Echo", err) + } + return decodeResponse(resp) + } +} diff --git a/app/api/v1/gen/http/teapot/client/encode_decode.go b/app/api/v1/gen/http/teapot/client/encode_decode.go new file mode 100644 index 0000000..e7adb3f --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/encode_decode.go @@ -0,0 +1,90 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot 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" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goahttp "goa.design/goa/v3/http" +) + +// BuildEchoRequest instantiates a HTTP request object with method and path set +// to call the "teapot" 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: EchoTeapotPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("teapot", "Echo", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeEchoRequest returns an encoder for requests sent to the teapot 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.(*teapot.EchoPayload) + if !ok { + return goahttp.ErrInvalidType("teapot", "Echo", "*teapot.EchoPayload", v) + } + body := NewEchoRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("teapot", "Echo", err) + } + return nil + } +} + +// DecodeEchoResponse returns a decoder for responses returned by the teapot +// Echo endpoint. restoreBody controls whether the response body should be +// restored after having been read. +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("teapot", "Echo", err) + } + err = ValidateEchoResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("teapot", "Echo", err) + } + res := NewEchoResultOK(&body) + return res, nil + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("teapot", "Echo", resp.StatusCode, string(body)) + } + } +} diff --git a/app/api/v1/gen/http/teapot/client/paths.go b/app/api/v1/gen/http/teapot/client/paths.go new file mode 100644 index 0000000..1efe5e6 --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/paths.go @@ -0,0 +1,13 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the teapot service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +// EchoTeapotPath returns the URL path to the teapot service Echo HTTP endpoint. +func EchoTeapotPath() string { + return "/echo" +} diff --git a/app/api/v1/gen/http/teapot/client/types.go b/app/api/v1/gen/http/teapot/client/types.go new file mode 100644 index 0000000..fb91ed1 --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/types.go @@ -0,0 +1,52 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goa "goa.design/goa/v3/pkg" +) + +// EchoRequestBody is the type of the "teapot" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// EchoResponseBody is the type of the "teapot" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` +} + +// NewEchoRequestBody builds the HTTP request body from the payload of the +// "Echo" endpoint of the "teapot" service. +func NewEchoRequestBody(p *teapot.EchoPayload) *EchoRequestBody { + body := &EchoRequestBody{ + Text: p.Text, + } + return body +} + +// NewEchoResultOK builds a "teapot" service "Echo" endpoint result from a HTTP +// "OK" response. +func NewEchoResultOK(body *EchoResponseBody) *teapot.EchoResult { + v := &teapot.EchoResult{ + Text: *body.Text, + } + + 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 +} diff --git a/app/api/v1/gen/http/teapot/server/encode_decode.go b/app/api/v1/gen/http/teapot/server/encode_decode.go new file mode 100644 index 0000000..1017d40 --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/encode_decode.go @@ -0,0 +1,60 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot 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" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// EncodeEchoResponse returns an encoder for responses returned by the teapot +// 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.(*teapot.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 teapot 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 + } +} diff --git a/app/api/v1/gen/http/teapot/server/paths.go b/app/api/v1/gen/http/teapot/server/paths.go new file mode 100644 index 0000000..264b16d --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/paths.go @@ -0,0 +1,13 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the teapot service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +// EchoTeapotPath returns the URL path to the teapot service Echo HTTP endpoint. +func EchoTeapotPath() string { + return "/echo" +} diff --git a/app/api/v1/gen/http/teapot/server/server.go b/app/api/v1/gen/http/teapot/server/server.go new file mode 100644 index 0000000..08546c6 --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/server.go @@ -0,0 +1,167 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "net/http" + "path" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Server lists the teapot service endpoint HTTP handlers. +type Server struct { + Mounts []*MountPoint + 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 teapot 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 *teapot.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{ + {"Echo", "POST", "/echo"}, + {"Serve gen/http/openapi3.json", "GET", "/openapi.json"}, + }, + 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 "teapot" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.Echo = m(s.Echo) +} + +// MethodNames returns the methods served. +func (s *Server) MethodNames() []string { return teapot.MethodNames[:] } + +// Mount configures the mux to serve the teapot endpoints. +func Mount(mux goahttp.Muxer, h *Server) { + MountEchoHandler(mux, h.Echo) + MountGenHTTPOpenapi3JSON(mux, h.GenHTTPOpenapi3JSON) +} + +// Mount configures the mux to serve the teapot endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountEchoHandler configures the mux to serve the "teapot" 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 "teapot" 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 = 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, "Echo") + ctx = context.WithValue(ctx, goa.ServiceKey, "teapot") + 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/teapot/server/types.go b/app/api/v1/gen/http/teapot/server/types.go new file mode 100644 index 0000000..114465f --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/types.go @@ -0,0 +1,51 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goa "goa.design/goa/v3/pkg" +) + +// EchoRequestBody is the type of the "teapot" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` +} + +// EchoResponseBody is the type of the "teapot" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// NewEchoResponseBody builds the HTTP response body from the result of the +// "Echo" endpoint of the "teapot" service. +func NewEchoResponseBody(res *teapot.EchoResult) *EchoResponseBody { + body := &EchoResponseBody{ + Text: res.Text, + } + return body +} + +// NewEchoPayload builds a teapot service Echo endpoint payload. +func NewEchoPayload(body *EchoRequestBody) *teapot.EchoPayload { + v := &teapot.EchoPayload{ + Text: *body.Text, + } + + return v +} + +// 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/web/client/cli.go b/app/api/v1/gen/http/web/client/cli.go new file mode 100644 index 0000000..5d68586 --- /dev/null +++ b/app/api/v1/gen/http/web/client/cli.go @@ -0,0 +1,61 @@ +// 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 + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" +) + +// BuildLoginGoogleCallbackPayload builds the payload for the web +// LoginGoogleCallback endpoint from CLI flags. +func BuildLoginGoogleCallbackPayload(webLoginGoogleCallbackCode string, webLoginGoogleCallbackState string, webLoginGoogleCallbackSessionCookie string) (*web.LoginGoogleCallbackPayload, error) { + var code string + { + code = webLoginGoogleCallbackCode + } + var state string + { + state = webLoginGoogleCallbackState + } + var sessionCookie string + { + sessionCookie = webLoginGoogleCallbackSessionCookie + } + v := &web.LoginGoogleCallbackPayload{} + v.Code = code + v.State = state + v.SessionCookie = sessionCookie + + return v, nil +} + +// BuildLogoutPayload builds the payload for the web Logout endpoint from CLI +// flags. +func BuildLogoutPayload(webLogoutSessionCookie string) (*web.LogoutPayload, error) { + var sessionCookie string + { + sessionCookie = webLogoutSessionCookie + } + v := &web.LogoutPayload{} + v.SessionCookie = sessionCookie + + return v, nil +} + +// BuildSessionTokenPayload builds the payload for the web SessionToken +// endpoint from CLI flags. +func BuildSessionTokenPayload(webSessionTokenSessionCookie string) (*web.SessionTokenPayload, error) { + var sessionCookie string + { + sessionCookie = webSessionTokenSessionCookie + } + v := &web.SessionTokenPayload{} + v.SessionCookie = sessionCookie + + return v, nil +} 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..5f40d5c --- /dev/null +++ b/app/api/v1/gen/http/web/client/client.go @@ -0,0 +1,203 @@ +// 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 + + // LoginGoogle Doer is the HTTP client used to make requests to the LoginGoogle + // endpoint. + LoginGoogleDoer goahttp.Doer + + // LoginGoogleCallback Doer is the HTTP client used to make requests to the + // LoginGoogleCallback endpoint. + LoginGoogleCallbackDoer goahttp.Doer + + // Logout Doer is the HTTP client used to make requests to the Logout endpoint. + LogoutDoer goahttp.Doer + + // SessionToken Doer is the HTTP client used to make requests to the + // SessionToken endpoint. + SessionTokenDoer 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, + LoginGoogleDoer: doer, + LoginGoogleCallbackDoer: doer, + LogoutDoer: doer, + SessionTokenDoer: 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) + } +} + +// LoginGoogle returns an endpoint that makes HTTP requests to the web service +// LoginGoogle server. +func (c *Client) LoginGoogle() goa.Endpoint { + var ( + decodeResponse = DecodeLoginGoogleResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLoginGoogleRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.LoginGoogleDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "LoginGoogle", err) + } + return decodeResponse(resp) + } +} + +// LoginGoogleCallback returns an endpoint that makes HTTP requests to the web +// service LoginGoogleCallback server. +func (c *Client) LoginGoogleCallback() goa.Endpoint { + var ( + encodeRequest = EncodeLoginGoogleCallbackRequest(c.encoder) + decodeResponse = DecodeLoginGoogleCallbackResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLoginGoogleCallbackRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.LoginGoogleCallbackDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "LoginGoogleCallback", err) + } + return decodeResponse(resp) + } +} + +// Logout returns an endpoint that makes HTTP requests to the web service +// Logout server. +func (c *Client) Logout() goa.Endpoint { + var ( + encodeRequest = EncodeLogoutRequest(c.encoder) + decodeResponse = DecodeLogoutResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLogoutRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.LogoutDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "Logout", err) + } + return decodeResponse(resp) + } +} + +// SessionToken returns an endpoint that makes HTTP requests to the web service +// SessionToken server. +func (c *Client) SessionToken() goa.Endpoint { + var ( + encodeRequest = EncodeSessionTokenRequest(c.encoder) + decodeResponse = DecodeSessionTokenResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildSessionTokenRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.SessionTokenDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "SessionToken", 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..2df36b0 --- /dev/null +++ b/app/api/v1/gen/http/web/client/encode_decode.go @@ -0,0 +1,551 @@ +// 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" + + web "github.com/jace-ys/countup/api/v1/gen/web" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// 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. +// DecodeIndexResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +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 + case http.StatusUnauthorized: + var ( + body IndexUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Index", err) + } + err = ValidateIndexUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Index", err) + } + return nil, NewIndexUnauthorized(&body) + 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. +// DecodeAnotherResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +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 + case http.StatusUnauthorized: + var ( + body AnotherUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Another", err) + } + err = ValidateAnotherUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Another", err) + } + return nil, NewAnotherUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "Another", resp.StatusCode, string(body)) + } + } +} + +// BuildLoginGoogleRequest instantiates a HTTP request object with method and +// path set to call the "web" service "LoginGoogle" endpoint +func (c *Client) BuildLoginGoogleRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LoginGoogleWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "LoginGoogle", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeLoginGoogleResponse returns a decoder for responses returned by the +// web LoginGoogle endpoint. restoreBody controls whether the response body +// should be restored after having been read. +// DecodeLoginGoogleResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLoginGoogleResponse(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.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogle", err) + } + res := NewLoginGoogleResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LoginGoogleUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "LoginGoogle", err) + } + err = ValidateLoginGoogleUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogle", err) + } + return nil, NewLoginGoogleUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "LoginGoogle", resp.StatusCode, string(body)) + } + } +} + +// BuildLoginGoogleCallbackRequest instantiates a HTTP request object with +// method and path set to call the "web" service "LoginGoogleCallback" endpoint +func (c *Client) BuildLoginGoogleCallbackRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LoginGoogleCallbackWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "LoginGoogleCallback", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeLoginGoogleCallbackRequest returns an encoder for requests sent to the +// web LoginGoogleCallback server. +func EncodeLoginGoogleCallbackRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.LoginGoogleCallbackPayload) + if !ok { + return goahttp.ErrInvalidType("web", "LoginGoogleCallback", "*web.LoginGoogleCallbackPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + values := req.URL.Query() + values.Add("code", p.Code) + values.Add("state", p.State) + req.URL.RawQuery = values.Encode() + return nil + } +} + +// DecodeLoginGoogleCallbackResponse returns a decoder for responses returned +// by the web LoginGoogleCallback endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeLoginGoogleCallbackResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLoginGoogleCallbackResponse(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.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogleCallback", err) + } + res := NewLoginGoogleCallbackResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LoginGoogleCallbackUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "LoginGoogleCallback", err) + } + err = ValidateLoginGoogleCallbackUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogleCallback", err) + } + return nil, NewLoginGoogleCallbackUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "LoginGoogleCallback", resp.StatusCode, string(body)) + } + } +} + +// BuildLogoutRequest instantiates a HTTP request object with method and path +// set to call the "web" service "Logout" endpoint +func (c *Client) BuildLogoutRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LogoutWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "Logout", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeLogoutRequest returns an encoder for requests sent to the web Logout +// server. +func EncodeLogoutRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.LogoutPayload) + if !ok { + return goahttp.ErrInvalidType("web", "Logout", "*web.LogoutPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + return nil + } +} + +// DecodeLogoutResponse returns a decoder for responses returned by the web +// Logout endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeLogoutResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLogoutResponse(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.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "Logout", err) + } + res := NewLogoutResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LogoutUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Logout", err) + } + err = ValidateLogoutUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Logout", err) + } + return nil, NewLogoutUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "Logout", resp.StatusCode, string(body)) + } + } +} + +// BuildSessionTokenRequest instantiates a HTTP request object with method and +// path set to call the "web" service "SessionToken" endpoint +func (c *Client) BuildSessionTokenRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: SessionTokenWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "SessionToken", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeSessionTokenRequest returns an encoder for requests sent to the web +// SessionToken server. +func EncodeSessionTokenRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.SessionTokenPayload) + if !ok { + return goahttp.ErrInvalidType("web", "SessionToken", "*web.SessionTokenPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + return nil + } +} + +// DecodeSessionTokenResponse returns a decoder for responses returned by the +// web SessionToken endpoint. restoreBody controls whether the response body +// should be restored after having been read. +// DecodeSessionTokenResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeSessionTokenResponse(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 SessionTokenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "SessionToken", err) + } + err = ValidateSessionTokenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "SessionToken", err) + } + res := NewSessionTokenResultOK(&body) + return res, nil + case http.StatusUnauthorized: + var ( + body SessionTokenUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "SessionToken", err) + } + err = ValidateSessionTokenUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "SessionToken", err) + } + return nil, NewSessionTokenUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "SessionToken", 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..1a563cf --- /dev/null +++ b/app/api/v1/gen/http/web/client/paths.go @@ -0,0 +1,38 @@ +// 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" +} + +// LoginGoogleWebPath returns the URL path to the web service LoginGoogle HTTP endpoint. +func LoginGoogleWebPath() string { + return "/login/google" +} + +// LoginGoogleCallbackWebPath returns the URL path to the web service LoginGoogleCallback HTTP endpoint. +func LoginGoogleCallbackWebPath() string { + return "/login/google/callback" +} + +// LogoutWebPath returns the URL path to the web service Logout HTTP endpoint. +func LogoutWebPath() string { + return "/logout" +} + +// SessionTokenWebPath returns the URL path to the web service SessionToken HTTP endpoint. +func SessionTokenWebPath() string { + return "/session/token" +} 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..3fda6cc --- /dev/null +++ b/app/api/v1/gen/http/web/client/types.go @@ -0,0 +1,410 @@ +// 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 + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" + goa "goa.design/goa/v3/pkg" +) + +// SessionTokenResponseBody is the type of the "web" service "SessionToken" +// endpoint HTTP response body. +type SessionTokenResponseBody struct { + Token *string `form:"token,omitempty" json:"token,omitempty" xml:"token,omitempty"` +} + +// IndexUnauthorizedResponseBody is the type of the "web" service "Index" +// endpoint HTTP response body for the "unauthorized" error. +type IndexUnauthorizedResponseBody 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"` +} + +// AnotherUnauthorizedResponseBody is the type of the "web" service "Another" +// endpoint HTTP response body for the "unauthorized" error. +type AnotherUnauthorizedResponseBody 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"` +} + +// LoginGoogleUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogle" endpoint HTTP response body for the "unauthorized" error. +type LoginGoogleUnauthorizedResponseBody 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"` +} + +// LoginGoogleCallbackUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogleCallback" endpoint HTTP response body for the "unauthorized" +// error. +type LoginGoogleCallbackUnauthorizedResponseBody 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"` +} + +// LogoutUnauthorizedResponseBody is the type of the "web" service "Logout" +// endpoint HTTP response body for the "unauthorized" error. +type LogoutUnauthorizedResponseBody 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"` +} + +// SessionTokenUnauthorizedResponseBody is the type of the "web" service +// "SessionToken" endpoint HTTP response body for the "unauthorized" error. +type SessionTokenUnauthorizedResponseBody 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"` +} + +// NewIndexUnauthorized builds a web service Index endpoint unauthorized error. +func NewIndexUnauthorized(body *IndexUnauthorizedResponseBody) *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 +} + +// NewAnotherUnauthorized builds a web service Another endpoint unauthorized +// error. +func NewAnotherUnauthorized(body *AnotherUnauthorizedResponseBody) *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 +} + +// NewLoginGoogleResultFound builds a "web" service "LoginGoogle" endpoint +// result from a HTTP "Found" response. +func NewLoginGoogleResultFound(redirectURL string, sessionCookie string) *web.LoginGoogleResult { + v := &web.LoginGoogleResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLoginGoogleUnauthorized builds a web service LoginGoogle endpoint +// unauthorized error. +func NewLoginGoogleUnauthorized(body *LoginGoogleUnauthorizedResponseBody) *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 +} + +// NewLoginGoogleCallbackResultFound builds a "web" service +// "LoginGoogleCallback" endpoint result from a HTTP "Found" response. +func NewLoginGoogleCallbackResultFound(redirectURL string, sessionCookie string) *web.LoginGoogleCallbackResult { + v := &web.LoginGoogleCallbackResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLoginGoogleCallbackUnauthorized builds a web service LoginGoogleCallback +// endpoint unauthorized error. +func NewLoginGoogleCallbackUnauthorized(body *LoginGoogleCallbackUnauthorizedResponseBody) *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 +} + +// NewLogoutResultFound builds a "web" service "Logout" endpoint result from a +// HTTP "Found" response. +func NewLogoutResultFound(redirectURL string, sessionCookie string) *web.LogoutResult { + v := &web.LogoutResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLogoutUnauthorized builds a web service Logout endpoint unauthorized +// error. +func NewLogoutUnauthorized(body *LogoutUnauthorizedResponseBody) *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 +} + +// NewSessionTokenResultOK builds a "web" service "SessionToken" endpoint +// result from a HTTP "OK" response. +func NewSessionTokenResultOK(body *SessionTokenResponseBody) *web.SessionTokenResult { + v := &web.SessionTokenResult{ + Token: *body.Token, + } + + return v +} + +// NewSessionTokenUnauthorized builds a web service SessionToken endpoint +// unauthorized error. +func NewSessionTokenUnauthorized(body *SessionTokenUnauthorizedResponseBody) *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 +} + +// ValidateSessionTokenResponseBody runs the validations defined on +// SessionTokenResponseBody +func ValidateSessionTokenResponseBody(body *SessionTokenResponseBody) (err error) { + if body.Token == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "body")) + } + return +} + +// ValidateIndexUnauthorizedResponseBody runs the validations defined on +// Index_unauthorized_Response_Body +func ValidateIndexUnauthorizedResponseBody(body *IndexUnauthorizedResponseBody) (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 +} + +// ValidateAnotherUnauthorizedResponseBody runs the validations defined on +// Another_unauthorized_Response_Body +func ValidateAnotherUnauthorizedResponseBody(body *AnotherUnauthorizedResponseBody) (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 +} + +// ValidateLoginGoogleUnauthorizedResponseBody runs the validations defined on +// LoginGoogle_unauthorized_Response_Body +func ValidateLoginGoogleUnauthorizedResponseBody(body *LoginGoogleUnauthorizedResponseBody) (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 +} + +// ValidateLoginGoogleCallbackUnauthorizedResponseBody runs the validations +// defined on LoginGoogleCallback_unauthorized_Response_Body +func ValidateLoginGoogleCallbackUnauthorizedResponseBody(body *LoginGoogleCallbackUnauthorizedResponseBody) (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 +} + +// ValidateLogoutUnauthorizedResponseBody runs the validations defined on +// Logout_unauthorized_Response_Body +func ValidateLogoutUnauthorizedResponseBody(body *LogoutUnauthorizedResponseBody) (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 +} + +// ValidateSessionTokenUnauthorizedResponseBody runs the validations defined on +// SessionToken_unauthorized_Response_Body +func ValidateSessionTokenUnauthorizedResponseBody(body *SessionTokenUnauthorizedResponseBody) (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/web/server/encode_decode.go b/app/api/v1/gen/http/web/server/encode_decode.go new file mode 100644 index 0000000..8b2bcd3 --- /dev/null +++ b/app/api/v1/gen/http/web/server/encode_decode.go @@ -0,0 +1,377 @@ +// 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" + "errors" + "net/http" + + web "github.com/jace-ys/countup/api/v1/gen/web" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// 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) + } +} + +// EncodeIndexError returns an encoder for errors returned by the Index web +// endpoint. +func EncodeIndexError(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 = NewIndexUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// 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) + } +} + +// EncodeAnotherError returns an encoder for errors returned by the Another web +// endpoint. +func EncodeAnotherError(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 = NewAnotherUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLoginGoogleResponse returns an encoder for responses returned by the +// web LoginGoogle endpoint. +func EncodeLoginGoogleResponse(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.(*web.LoginGoogleResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// EncodeLoginGoogleError returns an encoder for errors returned by the +// LoginGoogle web endpoint. +func EncodeLoginGoogleError(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 = NewLoginGoogleUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLoginGoogleCallbackResponse returns an encoder for responses returned +// by the web LoginGoogleCallback endpoint. +func EncodeLoginGoogleCallbackResponse(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.(*web.LoginGoogleCallbackResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// DecodeLoginGoogleCallbackRequest returns a decoder for requests sent to the +// web LoginGoogleCallback endpoint. +func DecodeLoginGoogleCallbackRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + code string + state string + sessionCookie string + err error + c *http.Cookie + ) + qp := r.URL.Query() + code = qp.Get("code") + if code == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "query string")) + } + state = qp.Get("state") + if state == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("state", "query string")) + } + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewLoginGoogleCallbackPayload(code, state, sessionCookie) + + return payload, nil + } +} + +// EncodeLoginGoogleCallbackError returns an encoder for errors returned by the +// LoginGoogleCallback web endpoint. +func EncodeLoginGoogleCallbackError(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 = NewLoginGoogleCallbackUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLogoutResponse returns an encoder for responses returned by the web +// Logout endpoint. +func EncodeLogoutResponse(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.(*web.LogoutResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// DecodeLogoutRequest returns a decoder for requests sent to the web Logout +// endpoint. +func DecodeLogoutRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + sessionCookie string + err error + c *http.Cookie + ) + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewLogoutPayload(sessionCookie) + + return payload, nil + } +} + +// EncodeLogoutError returns an encoder for errors returned by the Logout web +// endpoint. +func EncodeLogoutError(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 = NewLogoutUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeSessionTokenResponse returns an encoder for responses returned by the +// web SessionToken endpoint. +func EncodeSessionTokenResponse(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.(*web.SessionTokenResult) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "application/json") + enc := encoder(ctx, w) + body := NewSessionTokenResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeSessionTokenRequest returns a decoder for requests sent to the web +// SessionToken endpoint. +func DecodeSessionTokenRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + sessionCookie string + err error + c *http.Cookie + ) + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewSessionTokenPayload(sessionCookie) + + return payload, nil + } +} + +// EncodeSessionTokenError returns an encoder for errors returned by the +// SessionToken web endpoint. +func EncodeSessionTokenError(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 = NewSessionTokenUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} 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..ee25941 --- /dev/null +++ b/app/api/v1/gen/http/web/server/paths.go @@ -0,0 +1,38 @@ +// 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" +} + +// LoginGoogleWebPath returns the URL path to the web service LoginGoogle HTTP endpoint. +func LoginGoogleWebPath() string { + return "/login/google" +} + +// LoginGoogleCallbackWebPath returns the URL path to the web service LoginGoogleCallback HTTP endpoint. +func LoginGoogleCallbackWebPath() string { + return "/login/google/callback" +} + +// LogoutWebPath returns the URL path to the web service Logout HTTP endpoint. +func LogoutWebPath() string { + return "/logout" +} + +// SessionTokenWebPath returns the URL path to the web service SessionToken HTTP endpoint. +func SessionTokenWebPath() string { + return "/session/token" +} 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..94cb90a --- /dev/null +++ b/app/api/v1/gen/http/web/server/server.go @@ -0,0 +1,425 @@ +// 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 + LoginGoogle http.Handler + LoginGoogleCallback http.Handler + Logout http.Handler + SessionToken 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"}, + {"LoginGoogle", "GET", "/login/google"}, + {"LoginGoogleCallback", "GET", "/login/google/callback"}, + {"Logout", "GET", "/logout"}, + {"SessionToken", "GET", "/session/token"}, + {"Serve static/", "GET", "/static/*"}, + }, + Index: NewIndexHandler(e.Index, mux, decoder, encoder, errhandler, formatter), + Another: NewAnotherHandler(e.Another, mux, decoder, encoder, errhandler, formatter), + LoginGoogle: NewLoginGoogleHandler(e.LoginGoogle, mux, decoder, encoder, errhandler, formatter), + LoginGoogleCallback: NewLoginGoogleCallbackHandler(e.LoginGoogleCallback, mux, decoder, encoder, errhandler, formatter), + Logout: NewLogoutHandler(e.Logout, mux, decoder, encoder, errhandler, formatter), + SessionToken: NewSessionTokenHandler(e.SessionToken, 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) + s.LoginGoogle = m(s.LoginGoogle) + s.LoginGoogleCallback = m(s.LoginGoogleCallback) + s.Logout = m(s.Logout) + s.SessionToken = m(s.SessionToken) +} + +// 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) + MountLoginGoogleHandler(mux, h.LoginGoogle) + MountLoginGoogleCallbackHandler(mux, h.LoginGoogleCallback) + MountLogoutHandler(mux, h.Logout) + MountSessionTokenHandler(mux, h.SessionToken) + 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 = EncodeIndexError(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 = EncodeAnotherError(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) + } + }) +} + +// MountLoginGoogleHandler configures the mux to serve the "web" service +// "LoginGoogle" endpoint. +func MountLoginGoogleHandler(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", "/login/google", f) +} + +// NewLoginGoogleHandler creates a HTTP handler which loads the HTTP request +// and calls the "web" service "LoginGoogle" endpoint. +func NewLoginGoogleHandler( + 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 = EncodeLoginGoogleResponse(encoder) + encodeError = EncodeLoginGoogleError(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, "LoginGoogle") + 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) + } + }) +} + +// MountLoginGoogleCallbackHandler configures the mux to serve the "web" +// service "LoginGoogleCallback" endpoint. +func MountLoginGoogleCallbackHandler(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", "/login/google/callback", f) +} + +// NewLoginGoogleCallbackHandler creates a HTTP handler which loads the HTTP +// request and calls the "web" service "LoginGoogleCallback" endpoint. +func NewLoginGoogleCallbackHandler( + 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 = DecodeLoginGoogleCallbackRequest(mux, decoder) + encodeResponse = EncodeLoginGoogleCallbackResponse(encoder) + encodeError = EncodeLoginGoogleCallbackError(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, "LoginGoogleCallback") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + 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) + } + }) +} + +// MountLogoutHandler configures the mux to serve the "web" service "Logout" +// endpoint. +func MountLogoutHandler(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", "/logout", f) +} + +// NewLogoutHandler creates a HTTP handler which loads the HTTP request and +// calls the "web" service "Logout" endpoint. +func NewLogoutHandler( + 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 = DecodeLogoutRequest(mux, decoder) + encodeResponse = EncodeLogoutResponse(encoder) + encodeError = EncodeLogoutError(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, "Logout") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + 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) + } + }) +} + +// MountSessionTokenHandler configures the mux to serve the "web" service +// "SessionToken" endpoint. +func MountSessionTokenHandler(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", "/session/token", f) +} + +// NewSessionTokenHandler creates a HTTP handler which loads the HTTP request +// and calls the "web" service "SessionToken" endpoint. +func NewSessionTokenHandler( + 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 = DecodeSessionTokenRequest(mux, decoder) + encodeResponse = EncodeSessionTokenResponse(encoder) + encodeError = EncodeSessionTokenError(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, "SessionToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + 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 "/*": + name = "/static" + } + 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) +} 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..339a653 --- /dev/null +++ b/app/api/v1/gen/http/web/server/types.go @@ -0,0 +1,248 @@ +// 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 + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" + goa "goa.design/goa/v3/pkg" +) + +// SessionTokenResponseBody is the type of the "web" service "SessionToken" +// endpoint HTTP response body. +type SessionTokenResponseBody struct { + Token string `form:"token" json:"token" xml:"token"` +} + +// IndexUnauthorizedResponseBody is the type of the "web" service "Index" +// endpoint HTTP response body for the "unauthorized" error. +type IndexUnauthorizedResponseBody 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"` +} + +// AnotherUnauthorizedResponseBody is the type of the "web" service "Another" +// endpoint HTTP response body for the "unauthorized" error. +type AnotherUnauthorizedResponseBody 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"` +} + +// LoginGoogleUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogle" endpoint HTTP response body for the "unauthorized" error. +type LoginGoogleUnauthorizedResponseBody 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"` +} + +// LoginGoogleCallbackUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogleCallback" endpoint HTTP response body for the "unauthorized" +// error. +type LoginGoogleCallbackUnauthorizedResponseBody 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"` +} + +// LogoutUnauthorizedResponseBody is the type of the "web" service "Logout" +// endpoint HTTP response body for the "unauthorized" error. +type LogoutUnauthorizedResponseBody 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"` +} + +// SessionTokenUnauthorizedResponseBody is the type of the "web" service +// "SessionToken" endpoint HTTP response body for the "unauthorized" error. +type SessionTokenUnauthorizedResponseBody 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"` +} + +// NewSessionTokenResponseBody builds the HTTP response body from the result of +// the "SessionToken" endpoint of the "web" service. +func NewSessionTokenResponseBody(res *web.SessionTokenResult) *SessionTokenResponseBody { + body := &SessionTokenResponseBody{ + Token: res.Token, + } + return body +} + +// NewIndexUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Index" endpoint of the "web" service. +func NewIndexUnauthorizedResponseBody(res *goa.ServiceError) *IndexUnauthorizedResponseBody { + body := &IndexUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAnotherUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Another" endpoint of the "web" service. +func NewAnotherUnauthorizedResponseBody(res *goa.ServiceError) *AnotherUnauthorizedResponseBody { + body := &AnotherUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleUnauthorizedResponseBody builds the HTTP response body from +// the result of the "LoginGoogle" endpoint of the "web" service. +func NewLoginGoogleUnauthorizedResponseBody(res *goa.ServiceError) *LoginGoogleUnauthorizedResponseBody { + body := &LoginGoogleUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleCallbackUnauthorizedResponseBody builds the HTTP response body +// from the result of the "LoginGoogleCallback" endpoint of the "web" service. +func NewLoginGoogleCallbackUnauthorizedResponseBody(res *goa.ServiceError) *LoginGoogleCallbackUnauthorizedResponseBody { + body := &LoginGoogleCallbackUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLogoutUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Logout" endpoint of the "web" service. +func NewLogoutUnauthorizedResponseBody(res *goa.ServiceError) *LogoutUnauthorizedResponseBody { + body := &LogoutUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewSessionTokenUnauthorizedResponseBody builds the HTTP response body from +// the result of the "SessionToken" endpoint of the "web" service. +func NewSessionTokenUnauthorizedResponseBody(res *goa.ServiceError) *SessionTokenUnauthorizedResponseBody { + body := &SessionTokenUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleCallbackPayload builds a web service LoginGoogleCallback +// endpoint payload. +func NewLoginGoogleCallbackPayload(code string, state string, sessionCookie string) *web.LoginGoogleCallbackPayload { + v := &web.LoginGoogleCallbackPayload{} + v.Code = code + v.State = state + v.SessionCookie = sessionCookie + + return v +} + +// NewLogoutPayload builds a web service Logout endpoint payload. +func NewLogoutPayload(sessionCookie string) *web.LogoutPayload { + v := &web.LogoutPayload{} + v.SessionCookie = sessionCookie + + return v +} + +// NewSessionTokenPayload builds a web service SessionToken endpoint payload. +func NewSessionTokenPayload(sessionCookie string) *web.SessionTokenPayload { + v := &web.SessionTokenPayload{} + v.SessionCookie = sessionCookie + + return v +} diff --git a/app/api/v1/gen/teapot/client.go b/app/api/v1/gen/teapot/client.go new file mode 100644 index 0000000..7a51aae --- /dev/null +++ b/app/api/v1/gen/teapot/client.go @@ -0,0 +1,39 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package teapot + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Client is the "teapot" service client. +type Client struct { + EchoEndpoint goa.Endpoint +} + +// NewClient initializes a "teapot" service client given the endpoints. +func NewClient(echo goa.Endpoint) *Client { + return &Client{ + EchoEndpoint: echo, + } +} + +// Echo calls the "Echo" endpoint of the "teapot" service. +// Echo may return the following errors: +// - "unwell" (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/teapot/endpoints.go b/app/api/v1/gen/teapot/endpoints.go new file mode 100644 index 0000000..45c78c9 --- /dev/null +++ b/app/api/v1/gen/teapot/endpoints.go @@ -0,0 +1,40 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot endpoints +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package teapot + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Endpoints wraps the "teapot" service endpoints. +type Endpoints struct { + Echo goa.Endpoint +} + +// NewEndpoints wraps the methods of the "teapot" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + return &Endpoints{ + Echo: NewEchoEndpoint(s), + } +} + +// Use applies the given middleware to all the "teapot" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.Echo = m(e.Echo) +} + +// NewEchoEndpoint returns an endpoint function that calls the method "Echo" of +// service "teapot". +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/teapot/service.go b/app/api/v1/gen/teapot/service.go new file mode 100644 index 0000000..95c51f1 --- /dev/null +++ b/app/api/v1/gen/teapot/service.go @@ -0,0 +1,51 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot service +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package teapot + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Service is the teapot service interface. +type Service interface { + // 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 = "teapot" + +// 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 = [1]string{"Echo"} + +// EchoPayload is the payload type of the teapot service Echo method. +type EchoPayload struct { + Text string +} + +// EchoResult is the result type of the teapot service Echo method. +type EchoResult struct { + Text string +} + +// MakeUnwell builds a goa.ServiceError from an error. +func MakeUnwell(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unwell", false, false, false) +} diff --git a/app/api/v1/gen/web/client.go b/app/api/v1/gen/web/client.go new file mode 100644 index 0000000..2fc6620 --- /dev/null +++ b/app/api/v1/gen/web/client.go @@ -0,0 +1,115 @@ +// 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 + LoginGoogleEndpoint goa.Endpoint + LoginGoogleCallbackEndpoint goa.Endpoint + LogoutEndpoint goa.Endpoint + SessionTokenEndpoint goa.Endpoint +} + +// NewClient initializes a "web" service client given the endpoints. +func NewClient(index, another, loginGoogle, loginGoogleCallback, logout, sessionToken goa.Endpoint) *Client { + return &Client{ + IndexEndpoint: index, + AnotherEndpoint: another, + LoginGoogleEndpoint: loginGoogle, + LoginGoogleCallbackEndpoint: loginGoogleCallback, + LogoutEndpoint: logout, + SessionTokenEndpoint: sessionToken, + } +} + +// Index calls the "Index" endpoint of the "web" service. +// Index may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +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. +// Another may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +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 +} + +// LoginGoogle calls the "LoginGoogle" endpoint of the "web" service. +// LoginGoogle may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) LoginGoogle(ctx context.Context) (res *LoginGoogleResult, err error) { + var ires any + ires, err = c.LoginGoogleEndpoint(ctx, nil) + if err != nil { + return + } + return ires.(*LoginGoogleResult), nil +} + +// LoginGoogleCallback calls the "LoginGoogleCallback" endpoint of the "web" +// service. +// LoginGoogleCallback may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) LoginGoogleCallback(ctx context.Context, p *LoginGoogleCallbackPayload) (res *LoginGoogleCallbackResult, err error) { + var ires any + ires, err = c.LoginGoogleCallbackEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*LoginGoogleCallbackResult), nil +} + +// Logout calls the "Logout" endpoint of the "web" service. +// Logout may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) Logout(ctx context.Context, p *LogoutPayload) (res *LogoutResult, err error) { + var ires any + ires, err = c.LogoutEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*LogoutResult), nil +} + +// SessionToken calls the "SessionToken" endpoint of the "web" service. +// SessionToken may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) SessionToken(ctx context.Context, p *SessionTokenPayload) (res *SessionTokenResult, err error) { + var ires any + ires, err = c.SessionTokenEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*SessionTokenResult), 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..cd5e9db --- /dev/null +++ b/app/api/v1/gen/web/endpoints.go @@ -0,0 +1,97 @@ +// 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 + LoginGoogle goa.Endpoint + LoginGoogleCallback goa.Endpoint + Logout goa.Endpoint + SessionToken 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), + LoginGoogle: NewLoginGoogleEndpoint(s), + LoginGoogleCallback: NewLoginGoogleCallbackEndpoint(s), + Logout: NewLogoutEndpoint(s), + SessionToken: NewSessionTokenEndpoint(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) + e.LoginGoogle = m(e.LoginGoogle) + e.LoginGoogleCallback = m(e.LoginGoogleCallback) + e.Logout = m(e.Logout) + e.SessionToken = m(e.SessionToken) +} + +// 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) + } +} + +// NewLoginGoogleEndpoint returns an endpoint function that calls the method +// "LoginGoogle" of service "web". +func NewLoginGoogleEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.LoginGoogle(ctx) + } +} + +// NewLoginGoogleCallbackEndpoint returns an endpoint function that calls the +// method "LoginGoogleCallback" of service "web". +func NewLoginGoogleCallbackEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*LoginGoogleCallbackPayload) + return s.LoginGoogleCallback(ctx, p) + } +} + +// NewLogoutEndpoint returns an endpoint function that calls the method +// "Logout" of service "web". +func NewLogoutEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*LogoutPayload) + return s.Logout(ctx, p) + } +} + +// NewSessionTokenEndpoint returns an endpoint function that calls the method +// "SessionToken" of service "web". +func NewSessionTokenEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*SessionTokenPayload) + return s.SessionToken(ctx, p) + } +} diff --git a/app/api/v1/gen/web/service.go b/app/api/v1/gen/web/service.go new file mode 100644 index 0000000..40cbf10 --- /dev/null +++ b/app/api/v1/gen/web/service.go @@ -0,0 +1,94 @@ +// 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" + + goa "goa.design/goa/v3/pkg" +) + +// 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) + // LoginGoogle implements LoginGoogle. + LoginGoogle(context.Context) (res *LoginGoogleResult, err error) + // LoginGoogleCallback implements LoginGoogleCallback. + LoginGoogleCallback(context.Context, *LoginGoogleCallbackPayload) (res *LoginGoogleCallbackResult, err error) + // Logout implements Logout. + Logout(context.Context, *LogoutPayload) (res *LogoutResult, err error) + // SessionToken implements SessionToken. + SessionToken(context.Context, *SessionTokenPayload) (res *SessionTokenResult, 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 = [6]string{"Index", "Another", "LoginGoogle", "LoginGoogleCallback", "Logout", "SessionToken"} + +// LoginGoogleCallbackPayload is the payload type of the web service +// LoginGoogleCallback method. +type LoginGoogleCallbackPayload struct { + Code string + State string + SessionCookie string +} + +// LoginGoogleCallbackResult is the result type of the web service +// LoginGoogleCallback method. +type LoginGoogleCallbackResult struct { + RedirectURL string + SessionCookie string +} + +// LoginGoogleResult is the result type of the web service LoginGoogle method. +type LoginGoogleResult struct { + RedirectURL string + SessionCookie string +} + +// LogoutPayload is the payload type of the web service Logout method. +type LogoutPayload struct { + SessionCookie string +} + +// LogoutResult is the result type of the web service Logout method. +type LogoutResult struct { + RedirectURL string + SessionCookie string +} + +// SessionTokenPayload is the payload type of the web service SessionToken +// method. +type SessionTokenPayload struct { + SessionCookie string +} + +// SessionTokenResult is the result type of the web service SessionToken method. +type SessionTokenResult struct { + Token string +} + +// MakeUnauthorized builds a goa.ServiceError from an error. +func MakeUnauthorized(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unauthorized", false, false, false) +} diff --git a/app/atlas.hcl b/app/atlas.hcl new file mode 100644 index 0000000..1a1293d --- /dev/null +++ b/app/atlas.hcl @@ -0,0 +1,9 @@ +env "local" { + 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..f4c489d --- /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", "local-http", "Server host (valid values: local-http, local-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 "local-http": + addr = "http://localhost:8080" + case "local-grpc": + addr = "grpc://localhost:8081" + default: + fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: local-http|local-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 (local-http). valid values: local-http, local-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/main.go b/app/cmd/countup/main.go new file mode 100644 index 0000000..158aa8a --- /dev/null +++ b/app/cmd/countup/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "io" + "os" + + "github.com/alecthomas/kong" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/ctxlog" +) + +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."` +} + +type Globals struct { + Debug bool `env:"DEBUG" help:"Enable debug logging."` + Writer io.Writer `kong:"-"` +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + root := RootCmd{ + Globals: Globals{ + Writer: os.Stdout, + }, + } + + cli := kong.Parse(&root, + kong.Name(api.APIName), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{ + Compact: true, + FlagsLast: true, + NoExpandSubcommands: true, + }), + ) + + ctx = ctxlog.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..27de4c5 --- /dev/null +++ b/app/cmd/countup/migrate.go @@ -0,0 +1,51 @@ +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/schema/migrations" +) + +type MigrateCmd struct { + Command string `arg:"" help:"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 { + db, err := postgres.NewPool(ctx, c.Database.ConnectionURI) + if err != nil { + return fmt.Errorf("init db pool: %w", err) + } + defer db.Close() + + conn := stdlib.OpenDBFromPool(db.Pool) + defer conn.Close() + + if err := goose.SetDialect(string(goose.DialectPostgres)); err != nil { + return fmt.Errorf("set goose dialect: %w", 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..7a116f2 --- /dev/null +++ b/app/cmd/countup/server.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/gorilla/securecookie" + "github.com/markbates/goth/providers/google" + + apiv1 "github.com/jace-ys/countup/api/v1" + genapi "github.com/jace-ys/countup/api/v1/gen/api" + grpcapiclient "github.com/jace-ys/countup/api/v1/gen/grpc/api/client" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + grpcapi "github.com/jace-ys/countup/api/v1/gen/grpc/api/server" + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + grpcteapot "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/server" + httpapi "github.com/jace-ys/countup/api/v1/gen/http/api/server" + httpteapot "github.com/jace-ys/countup/api/v1/gen/http/teapot/server" + httpweb "github.com/jace-ys/countup/api/v1/gen/http/web/server" + genteapot "github.com/jace-ys/countup/api/v1/gen/teapot" + genweb "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/app" + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/endpoint" + "github.com/jace-ys/countup/internal/handler/api" + "github.com/jace-ys/countup/internal/handler/teapot" + "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/service/user" + userstore "github.com/jace-ys/countup/internal/service/user/store" + "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."` + + OAuth struct { + ClientID string `env:"CLIENT_ID" required:"" help:"Client ID for the Google OAuth2 configuration."` + ClientSecret string `env:"CLIENT_SECRET" required:"" help:"Client secret for the Google OAuth2 configuration."` + RedirectURL string `env:"REDIRECT_URL" default:"http://localhost:8080/login/google/callback" help:"URL to redirect to upon successful OAuth2 authentication."` + } `embed:"" envprefix:"OAUTH_" prefix:"oauth."` + + 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 { + instrument.MustInitOTelProvider(ctx, genapi.APIName, genapi.APIVersion, c.OTLP.MetricsEndpoint, c.OTLP.TracesEndpoint) + defer func() { + if err := instrument.OTel.Shutdown(ctx); err != nil { + ctxlog.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, c.Worker.Concurrency) + if err != nil { + return fmt.Errorf("init worker pool: %w", err) + } + + authn := google.New(c.OAuth.ClientID, c.OAuth.ClientSecret, c.OAuth.RedirectURL) + + 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 := user.New(db, userstore.New()) + + handler, err := api.NewHandler(authn, counterSvc, userSvc) + if err != nil { + return fmt.Errorf("init api handler: %w", err) + } + admin.Administer(handler) + + ep := endpoint.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 := teapot.NewHandler(worker) + if err != nil { + return fmt.Errorf("init teapot handler: %w", err) + } + admin.Administer(handler) + + ep := endpoint.Goa(genteapot.NewEndpoints).Adapt(handler) + + { + transport := transport.GoaHTTP(httpteapot.New, httpteapot.Mount) + httpSrv.RegisterHandler("/teapot", transport.Adapt(ctx, ep, apiv1.OpenAPIFS)) + } + { + transport := transport.GoaGRPC(grpcteapot.New) + grpcSrv.RegisterHandler(&teapotpb.Teapot_ServiceDesc, transport.Adapt(ctx, ep)) + } + } + + { + cookies := securecookie.New([]byte("secret-hash-key"), []byte("secret-block-key")) + + tc, err := transport.GoaGRPCClient(grpcapiclient.NewClient).Adapt(grpcSrv.Addr()) + if err != nil { + return fmt.Errorf("init api client: %w", err) + } + defer tc.Close() + + apiClient := genapi.NewClient(tc.Client().AuthToken(), tc.Client().CounterGet(), tc.Client().CounterIncrement()) + + handler, err := web.NewHandler(authn, cookies, apiClient) + if err != nil { + return fmt.Errorf("init web handler: %w", err) + } + admin.Administer(handler) + + ep := endpoint.Goa(genweb.NewEndpoints).Adapt(handler) + + transport := transport.GoaHTTP(httpweb.New, httpweb.Mount) + httpSrv.RegisterHandler("/", transport.Adapt(ctx, ep, web.StaticFS)) + } + + if err := app.New(httpSrv, grpcSrv, admin, worker).Run(ctx); err != nil { + ctxlog.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..5efaade --- /dev/null +++ b/app/go.mod @@ -0,0 +1,83 @@ +module github.com/jace-ys/countup + +go 1.23.4 + +require ( + github.com/alecthomas/kong v1.6.0 + github.com/alexliesenfeld/health v0.8.0 + github.com/exaring/otelpgx v0.7.0 + github.com/go-chi/chi/v5 v5.1.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/gorilla/securecookie v1.1.2 + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/jackc/pgx/v5 v5.7.1 + github.com/markbates/goth v1.80.0 + github.com/pressly/goose/v3 v3.23.0 + github.com/riverqueue/river v0.14.2 + github.com/riverqueue/river/riverdriver/riverdatabasesql v0.14.2 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.14.2 + github.com/riverqueue/river/rivertype v0.14.2 + github.com/rs/xid v1.6.0 + github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 + go.opentelemetry.io/otel/metric v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 + goa.design/clue v1.0.7 + goa.design/goa/v3 v3.19.1 + golang.org/x/sync v0.10.0 + google.golang.org/grpc v1.68.1 + google.golang.org/protobuf v1.35.2 +) + +require ( + cloud.google.com/go/compute/metadata v0.5.2 // indirect + github.com/aws/smithy-go v1.22.1 // 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.24.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/lib/pq v1.10.9 // 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.14.2 // indirect + github.com/riverqueue/river/rivershared v0.14.2 // 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.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/genproto v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // 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..1806910 --- /dev/null +++ b/app/go.sum @@ -0,0 +1,211 @@ +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE= +github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +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.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/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.7.0 h1:Wv1x53y6zmmBsEPbWNae6XJAbMNC3KSJmpWRoZxtZr8= +github.com/exaring/otelpgx v0.7.0/go.mod h1:2oRpYkkPBXpvRqQqP0gqkkFPwITRObbpsrA8NT1Fu/I= +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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +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.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +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/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= +github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= +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.23.0 h1:57hqKos8izGek4v6D5+OXBa+Y4Rq8MU//+MmnevdpVA= +github.com/pressly/goose/v3 v3.23.0/go.mod h1:rpx+D9GX/+stXmzKa+uh1DkjPnNVMdiOCV9iLdle4N8= +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.14.2 h1:I2VJ5HawamDDiL7QIy1XF2/PtwC+Re2y0OBR9Si6v/s= +github.com/riverqueue/river v0.14.2/go.mod h1:RHcZSKQuaYfylhbkIHcw+xUdBia3LtPJr66qsFCMj3Q= +github.com/riverqueue/river/riverdriver v0.14.2 h1:7WSg3m8gjMbmwMeavBklkABLqy/+ox+Zg7d96zT0yBc= +github.com/riverqueue/river/riverdriver v0.14.2/go.mod h1:Z3mKValBmOfJGfOxd7FWeAQDR9tFkpYObqNS0BSJtMU= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.14.2 h1:SRUAmpZBIwJj8P8l6DK86z1jp7o31YSg4aresvnC8yc= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.14.2/go.mod h1:ThZfeDtiphBt3a0iwFQTy7uVTQ1DliC9cXdwNaPDQ2w= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.14.2 h1:+zhN3LGdzEoi/EmmNxMkunmeFd84XAB8ykDD8Rfqoq8= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.14.2/go.mod h1:OrpOHxAy4wOHGK2bwFKyOgKG353e2Z2EaPwO6RWlZp4= +github.com/riverqueue/river/rivershared v0.14.2 h1:5cUC3jXYi1zLg7tPjsOwDPj9KQuxM1Mf5QBRS6QFHkc= +github.com/riverqueue/river/rivershared v0.14.2/go.mod h1:WZnOZV9KQgittVA01UH3/GI9RSgG0JfDkA/cohqV7v0= +github.com/riverqueue/river/rivertype v0.14.2 h1:otCEcibq2y5+HAxqvVPpc4tgShwISbFWrtqyL8qnI0M= +github.com/riverqueue/river/rivertype v0.14.2/go.mod h1:4vpt5ZSdZ35mFbRAV4oXgeRdH3Mq5h1pUzQTvaGfCUA= +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/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.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.57.0 h1:qtFISDHKolvIxzSs0gIaiPUPR0Cucb0F2coHC7ZLdps= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0/go.mod h1:Y+Pop1Q6hCOnETWTW4NROK/q1hv50hM7yDaUTjG8lp8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +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.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +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.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +google.golang.org/genproto v0.0.0-20241206012308-a4fef0638583 h1:pjPnE7Rv3PAwHISLRJhA3HQTnM2uu5qcnroxTkRb5G8= +google.golang.org/genproto v0.0.0-20241206012308-a4fef0638583/go.mod h1:dW27OyXi0Ph+N43jeCWMFC86aTT5VgdeQtOSf0Hehdw= +google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 h1:v+j+5gpj0FopU0KKLDGfDo9ZRRpKdi5UBrCP0f76kuY= +google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/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/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +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.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk= +modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +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..75b7ddc --- /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" + "goa.design/goa/v3/http/middleware" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/healthz" + "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...))) + + debugMux := goahttp.NewMuxer() + debug.MountPprofHandlers(debug.Adapt(debugMux), debug.WithPrefix("/pprof")) + if s.debug { + debug.MountDebugLogEnabler(debug.Adapt(debugMux), debug.WithPath("/settings")) + } + s.mux.Mount("/debug", debugMux) + + logCtx := log.With(ctx, ctxlog.KV("server", s.Name())) + return chainMiddleware(s.mux, + recovery.HTTP(logCtx), + middleware.PopulateRequestContext(), + ) +} + +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..fc1e1c2 --- /dev/null +++ b/app/internal/app/application.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + "os/signal" + "syscall" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/jace-ys/countup/internal/ctxlog" +) + +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 { + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-ctx.Done() + stop() + }() + + g, ctx := errgroup.WithContext(ctx) + + for _, srv := range a.servers { + g.Go(func() error { + ctxlog.Print(ctx, "server listening", + ctxlog.KV("server", srv.Name()), + ctxlog.KV("kind", srv.Kind()), + ctxlog.KV("addr", srv.Addr()), + ) + return srv.Serve(ctx) + }) + } + + ctxlog.Print(ctx, "application started") + <-ctx.Done() + ctxlog.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 { + ctxlog.Error(ctx, "server shutdown error", err, + ctxlog.KV("server", srv.Name()), + ctxlog.KV("kind", srv.Kind()), + ) + } else { + ctxlog.Print(ctx, "server shutdown complete", + ctxlog.KV("server", srv.Name()), + ctxlog.KV("kind", srv.Kind()), + ) + } + return nil + }) + } + + defer ctxlog.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..7c18821 --- /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/ctxlog" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +type GRPCServer struct { + name string + addr string + srv *grpc.Server +} + +func NewGRPCServer[SS any](ctx context.Context, name string, port int) *GRPCServer { + 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, ctxlog.KV("server", name)) + srv := grpc.NewServer( + grpc.ChainUnaryInterceptor( + recovery.UnaryServerInterceptor(logCtx), + withMethodFilter(reqid.UnaryServerInterceptor(), excludedMethods), + withMethodFilter(ctxlog.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{ + 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) RegisterHandler(sd *grpc.ServiceDesc, ss any) { + s.srv.RegisterService(sd, ss) +} + +var _ Server = (*GRPCServer)(nil) + +func (s *GRPCServer) Name() string { + return s.name +} + +func (s *GRPCServer) Kind() string { + return "grpc" +} + +func (s *GRPCServer) Addr() string { + return s.addr +} + +func (s *GRPCServer) 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) 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() + } +} + +var _ healthz.Target = (*GRPCServer)(nil) + +func (s *GRPCServer) 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..5c0a176 --- /dev/null +++ b/app/internal/app/http.go @@ -0,0 +1,119 @@ +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" + "goa.design/goa/v3/http/middleware" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" + "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) { + pattern := path + "*" + s.mux.Handle(pattern, 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, ctxlog.KV("server", s.Name())) + return chainMiddleware(s.mux, + withPathFilter(telemetry.HTTP(attribute.String("http.server.name", s.Name())), excludedPaths), + recovery.HTTP(logCtx), + withPathFilter(middleware.PopulateRequestContext(), excludedPaths), + withPathFilter(reqid.HTTP(), excludedPaths), + withPathFilter(ctxlog.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/ctxlog/clue.go b/app/internal/ctxlog/clue.go new file mode 100644 index 0000000..5852095 --- /dev/null +++ b/app/internal/ctxlog/clue.go @@ -0,0 +1,63 @@ +package ctxlog + +import ( + "context" + "io" + + "goa.design/clue/log" +) + +func NewContext(ctx context.Context, w io.Writer, debug bool) context.Context { + format := log.FormatJSON + if log.IsTerminal() { + format = log.FormatTerminal + } + + opts := []log.LogOption{ + log.WithOutput(w), + log.WithFormat(format), + log.WithFileLocation(), + log.WithFunc(log.Span), + } + + if debug { + opts = append(opts, log.WithDebug()) + } + + return log.Context(ctx, opts...) +} + +func KV(key string, val any) log.KV { + return log.KV{K: key, V: val} +} + +func With(ctx context.Context, kv ...log.Fielder) context.Context { + return log.With(ctx, kv...) +} + +func Print(ctx context.Context, msg string, kv ...log.Fielder) { + log.Print(ctx, buildKVs(msg, kv...)...) +} + +func Debug(ctx context.Context, msg string, kv ...log.Fielder) { + log.Debug(ctx, buildKVs(msg, kv...)...) +} + +func Info(ctx context.Context, msg string, kv ...log.Fielder) { + log.Info(ctx, buildKVs(msg, kv...)...) +} + +func Error(ctx context.Context, msg string, err error, kv ...log.Fielder) { + log.Error(ctx, err, buildKVs(msg, kv...)...) +} + +func Fatal(ctx context.Context, msg string, err error, kv ...log.Fielder) { + log.Fatal(ctx, err, buildKVs(msg, kv...)...) +} + +func buildKVs(msg string, kv ...log.Fielder) []log.Fielder { + kvs := make([]log.Fielder, 0, len(kv)+1) + kvs = append(kvs, KV(log.MessageKey, msg)) + kvs = append(kvs, kv...) + return kvs +} diff --git a/app/internal/ctxlog/middleware.go b/app/internal/ctxlog/middleware.go new file mode 100644 index 0000000..4760661 --- /dev/null +++ b/app/internal/ctxlog/middleware.go @@ -0,0 +1,47 @@ +package ctxlog + +import ( + "context" + "net/http" + + "goa.design/clue/log" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +func UnaryServerInterceptor(logCtx context.Context) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, next grpc.UnaryHandler) (_ any, err error) { + requestID := reqid.RequestIDFromContext(ctx) + + ctx = log.WithContext(ctx, logCtx) + ctx = log.With(ctx, KV(log.RequestIDKey, requestID)) + + opts := []log.GRPCLogOption{ + log.WithErrorFunc(func(c codes.Code) bool { return false }), + log.WithDisableCallID(), + } + + return log.UnaryServerInterceptor(ctx, opts...)(ctx, req, info, next) + } +} + +func HTTP(logCtx context.Context) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + requestID := reqid.RequestIDFromContext(ctx) + + ctx = log.WithContext(ctx, logCtx) + ctx = log.With(ctx, KV(log.RequestIDKey, requestID)) + + opts := []log.HTTPLogOption{ + log.WithDisableRequestID(), + } + + handler := log.HTTP(ctx, opts...)(next) + handler.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/app/internal/ctxlog/slog.go b/app/internal/ctxlog/slog.go new file mode 100644 index 0000000..b720ee1 --- /dev/null +++ b/app/internal/ctxlog/slog.go @@ -0,0 +1,105 @@ +package ctxlog + +import ( + "context" + "errors" + "log/slog" + + "goa.design/clue/log" +) + +type SlogHandler struct { + ctx context.Context + level slog.Level + group string +} + +func AsSlogHandler(ctx context.Context, level slog.Level) *SlogHandler { + return &SlogHandler{ + ctx: log.Context(ctx, log.WithDebug()), + level: level, + } +} + +var _ slog.Handler = (*SlogHandler)(nil) + +func (l *SlogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return level >= l.level +} + +func (l *SlogHandler) Handle(ctx context.Context, record slog.Record) error { + var attrs []log.Fielder + record.Attrs(func(a slog.Attr) bool { + attrs = append(attrs, KV(a.Key, a.Value.Any())) + return true + }) + + logCtx := log.WithContext(ctx, l.ctx) + logCtx = log.With(logCtx, attrs...) + + switch record.Level { + case slog.LevelDebug: + Debug(logCtx, record.Message) + case slog.LevelInfo: + Info(logCtx, record.Message) + case slog.LevelWarn: + Error(logCtx, "warn", errors.New(record.Message)) + case slog.LevelError: + Error(logCtx, "error", errors.New(record.Message)) + default: + Print(logCtx, record.Message) + } + + return nil +} + +func (l *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + kvs := make([]log.Fielder, len(attrs)) + for i, attr := range attrs { + key := attr.Key + if l.group != "" { + key = l.group + "." + attr.Key + } + kvs[i] = KV(key, attr.Value.Any()) + } + + cp := l.clone() + cp.ctx = log.With(cp.ctx, kvs...) + return cp +} + +func (l *SlogHandler) WithGroup(name string) slog.Handler { + cp := l.clone() + cp.group = name + return cp +} + +func (l *SlogHandler) clone() *SlogHandler { + cp := *l + return &cp +} + +type NopHandler struct { +} + +func AsNopHandler() *NopHandler { + return &NopHandler{} +} + +var _ slog.Handler = (*NopHandler)(nil) + +func (l NopHandler) Enabled(ctx context.Context, level slog.Level) bool { + return false +} + +func (l NopHandler) Handle(ctx context.Context, record slog.Record) error { + return nil +} + +func (l NopHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return l +} + +func (l NopHandler) WithGroup(name string) slog.Handler { + return l +} diff --git a/app/internal/ctxlog/slog_test.go b/app/internal/ctxlog/slog_test.go new file mode 100644 index 0000000..6589233 --- /dev/null +++ b/app/internal/ctxlog/slog_test.go @@ -0,0 +1,184 @@ +package ctxlog_test + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "io" + "log/slog" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jace-ys/countup/internal/ctxlog" +) + +func TestSlogHandler(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.Debug("test debug", "count", 1.0) + logger.Info("test info", "count", 2.0) + logger.Warn("test warn", "count", 3.0) + logger.Error("test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0}, + {"level": "info", "msg": "test info", "count": 2.0}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + }) +} + +func TestSlogHandlerLevel(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelWarn)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + }) +} + +func TestSlogHandlerContext(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + ctx = ctxlog.With(ctx, ctxlog.KV("foo", "bar")) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + ctx = ctxlog.With(ctx, ctxlog.KV("ping", "pong")) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0, "foo": "bar"}, + {"level": "info", "msg": "test info", "count": 2.0, "foo": "bar"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "foo": "bar"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "foo": "bar"}, + {"level": "debug", "msg": "test debug", "count": 1.0, "foo": "bar"}, + {"level": "info", "msg": "test info", "count": 2.0, "foo": "bar"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "foo": "bar"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "foo": "bar"}, + }) +} + +func TestSlogHandlerWith(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + logger = logger.With("ping", "pong") + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0}, + {"level": "info", "msg": "test info", "count": 2.0}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + {"level": "debug", "msg": "test debug", "count": 1.0, "ping": "pong"}, + {"level": "info", "msg": "test info", "count": 2.0, "ping": "pong"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "ping": "pong"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "ping": "pong"}, + }) +} + +func TestSlogHandlerGroup(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + logger = logger.WithGroup("foo").With("bar", "baz").WithGroup("ping").With("pong", "table") + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0}, + {"level": "info", "msg": "test info", "count": 2.0}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + {"level": "debug", "msg": "test debug", "count": 1.0, "foo.bar": "baz", "ping.pong": "table"}, + {"level": "info", "msg": "test info", "count": 2.0, "foo.bar": "baz", "ping.pong": "table"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "foo.bar": "baz", "ping.pong": "table"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "foo.bar": "baz", "ping.pong": "table"}, + }) +} + +func TestSlogHandlerConcurrent(t *testing.T) { + ctx := ctxlog.NewContext(context.Background(), io.Discard, false) + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + var wg sync.WaitGroup + for i := range 100 { + wg.Add(1) + go func(id int) { + defer wg.Done() + + if id%2 == 0 { + logger.Info("table", "ping", "pong") + } else { + logger.WithGroup("foo").With("bar", "baz").Info("table", "ping", "pong") + } + }(i) + } + + wg.Wait() +} + +func assertLogOutput(t *testing.T, r io.Reader, expected []map[string]any) { + scanner := bufio.NewScanner(r) + var actual []map[string]any + + for scanner.Scan() { + var data map[string]any + require.NoError(t, json.Unmarshal(scanner.Bytes(), &data)) + delete(data, "time") + delete(data, "file") + actual = append(actual, data) + } + + require.Len(t, actual, len(expected)) + + for i, data := range actual { + assert.Equal(t, expected[i], data) + } +} diff --git a/app/internal/endpoint/goa.go b/app/internal/endpoint/goa.go new file mode 100644 index 0000000..b3428ad --- /dev/null +++ b/app/internal/endpoint/goa.go @@ -0,0 +1,45 @@ +package endpoint + +import ( + "goa.design/clue/debug" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/endpoint/middleware/goaerror" + "github.com/jace-ys/countup/internal/endpoint/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/endpoint/middleware/goaerror/reporter.go b/app/internal/endpoint/middleware/goaerror/reporter.go new file mode 100644 index 0000000..e1e9762 --- /dev/null +++ b/app/internal/endpoint/middleware/goaerror/reporter.go @@ -0,0 +1,37 @@ +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/ctxlog" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +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 = reqid.RequestIDFromContext(ctx) + + ctxlog.Error(ctx, "endpoint error", err, ctxlog.KV("err.name", gerr.Name)) + + span := trace.SpanFromContext(ctx) + span.SetStatus(codes.Error, gerr.Name) + span.SetAttributes(attribute.String("error", err.Error())) + + return res, gerr + } +} diff --git a/app/internal/endpoint/middleware/tracer/endpoint.go b/app/internal/endpoint/middleware/tracer/endpoint.go new file mode 100644 index 0000000..1d70ce1 --- /dev/null +++ b/app/internal/endpoint/middleware/tracer/endpoint.go @@ -0,0 +1,32 @@ +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 + } + + source := fmt.Sprintf("goa.endpoint/%s.%s", service, method) + ctx, span := instrument.OTel.Tracer().Start(ctx, source) + 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/auth.go b/app/internal/handler/api/auth.go new file mode 100644 index 0000000..114f7ab --- /dev/null +++ b/app/internal/handler/api/auth.go @@ -0,0 +1,116 @@ +package api + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/markbates/goth/providers/google" + goa "goa.design/goa/v3/pkg" + "goa.design/goa/v3/security" + + apiv1 "github.com/jace-ys/countup/api/v1" + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/service/user" +) + +const ( + jwtClaimsIssuer = "countup.api.AuthToken" + jwtClaimsAudience = "countup.api.JWTAuth" +) + +type AuthTokenClaims struct { + jwt.RegisteredClaims + + Scopes jwt.ClaimStrings `json:"scopes"` +} + +func (h *Handler) AuthToken(ctx context.Context, req *api.AuthTokenPayload) (*api.AuthTokenResult, error) { + session := &google.Session{ + AccessToken: req.AccessToken, + } + + fetchedUser, err := h.authn.FetchUser(session) + if err != nil { + return nil, goa.Fault("fetch user context from provider %s: %s", req.Provider, err) + } + + if fetchedUser.Email == "" { + return nil, api.MakeIncompleteAuthInfo(errors.New("missing field in fetched user context: email")) + } + + user, err := h.users.CreateUserIfNotExists(ctx, fetchedUser.Email) + if err != nil { + return nil, goa.Fault("create new user: %s", err) + } + + now := time.Now() + + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, AuthTokenClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: jwtClaimsIssuer, + Audience: jwt.ClaimStrings{jwtClaimsAudience}, + Subject: user.ID, + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), + }, + Scopes: jwt.ClaimStrings{apiv1.AuthScopeAPIUser}, + }) + + tok, err := claims.SignedString(h.jwtSigningSecret) + if err != nil { + return nil, goa.Fault("sign JWT token: %s", err) + } + + return &api.AuthTokenResult{ + Token: tok, + }, nil +} + +var _ api.Auther = (*Handler)(nil) + +func (h *Handler) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) { + parser := jwt.NewParser( + jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), + jwt.WithIssuer(jwtClaimsIssuer), + jwt.WithAudience(jwtClaimsAudience), + jwt.WithIssuedAt(), + jwt.WithExpirationRequired(), + ) + + var claims AuthTokenClaims + tok, err := parser.ParseWithClaims(token, &claims, func(t *jwt.Token) (interface{}, error) { + return h.jwtSigningSecret, nil + }) + if err != nil { + return nil, api.MakeForbidden(fmt.Errorf("parse JWT auth token: %w", err)) + } + + if !tok.Valid { + return nil, api.MakeForbidden(errors.New("invalid JWT auth token")) + } + + var missingScopes []string + for _, scope := range scheme.RequiredScopes { + if !slices.Contains(claims.Scopes, scope) { + missingScopes = append(missingScopes, scope) + } + } + + if len(missingScopes) > 0 { + return nil, api.MakeForbidden( + fmt.Errorf("missing scopes in JWT auth token: %v", missingScopes), + ) + } + + usr, err := h.users.GetUser(ctx, claims.Subject) + if err != nil { + return nil, api.MakeForbidden(errors.New("access denied")) + } + + return user.ContextWithUser(ctx, usr), nil +} diff --git a/app/internal/handler/api/counter.go b/app/internal/handler/api/counter.go new file mode 100644 index 0000000..3656554 --- /dev/null +++ b/app/internal/handler/api/counter.go @@ -0,0 +1,43 @@ +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" + "github.com/jace-ys/countup/internal/service/user" +) + +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) { + usr := user.UserFromContext(ctx) + + if err := h.counter.RequestIncrement(ctx, usr.Email); err != nil { + switch { + case errors.Is(err, &counter.MultipleIncrementRequestError{}): + return nil, api.MakeExistingIncrementRequest( + errors.New("user already made an increment request in the recent finalize window, please try again after the next finalize time"), + ) + default: + return nil, goa.Fault("request increment: %s", err) + } + } + + return h.CounterGet(ctx) +} diff --git a/app/internal/handler/api/handler.go b/app/internal/handler/api/handler.go new file mode 100644 index 0000000..0d4870a --- /dev/null +++ b/app/internal/handler/api/handler.go @@ -0,0 +1,55 @@ +package api + +import ( + "context" + + "github.com/alexliesenfeld/health" + "github.com/markbates/goth" + + "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/service/user" +) + +var _ api.Service = (*Handler)(nil) + +type Handler struct { + authn goth.Provider + counter CounterService + users UserService + + jwtSigningSecret []byte +} + +type CounterService interface { + GetInfo(ctx context.Context) (*counter.Info, error) + RequestIncrement(ctx context.Context, user string) error +} + +type UserService interface { + GetUser(ctx context.Context, id string) (*user.User, error) + CreateUserIfNotExists(ctx context.Context, email string) (*user.User, error) +} + +func NewHandler(authn goth.Provider, counter CounterService, users UserService) (*Handler, error) { + return &Handler{ + authn: authn, + counter: counter, + users: users, + jwtSigningSecret: []byte("secret"), + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:api", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} diff --git a/app/internal/handler/teapot/echo.go b/app/internal/handler/teapot/echo.go new file mode 100644 index 0000000..004725f --- /dev/null +++ b/app/internal/handler/teapot/echo.go @@ -0,0 +1,72 @@ +package teapot + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/riverqueue/river" + + "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +func (h *Handler) Echo(ctx context.Context, req *teapot.EchoPayload) (*teapot.EchoResult, error) { + switch { + case strings.HasPrefix(req.Text, "error: "): + msg := strings.TrimPrefix(req.Text, "error: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Error: msg}) + return nil, teapot.MakeUnwell(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, teapot.MakeUnwell(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 &teapot.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/teapot/handler.go b/app/internal/handler/teapot/handler.go new file mode 100644 index 0000000..3c71afb --- /dev/null +++ b/app/internal/handler/teapot/handler.go @@ -0,0 +1,38 @@ +package teapot + +import ( + "context" + + "github.com/alexliesenfeld/health" + + "github.com/jace-ys/countup/api/v1/gen/teapot" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/worker" +) + +var _ teapot.Service = (*Handler)(nil) + +type Handler struct { + workers *worker.Pool +} + +func NewHandler(workers *worker.Pool) (*Handler, error) { + worker.Register(workers, &EchoWorker{}) + + return &Handler{ + workers: workers, + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:teapot", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} diff --git a/app/internal/handler/web/authn.go b/app/internal/handler/web/authn.go new file mode 100644 index 0000000..e038b02 --- /dev/null +++ b/app/internal/handler/web/authn.go @@ -0,0 +1,119 @@ +package web + +import ( + "context" + "errors" + "fmt" + "net/url" + + goa "goa.design/goa/v3/pkg" + + apiv1 "github.com/jace-ys/countup/api/v1" + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +const sessionName = "countup.session" + +type authSession struct { + State string + Token string + Session string +} + +func (h *Handler) LoginGoogle(ctx context.Context) (*web.LoginGoogleResult, error) { + reqID := reqid.RequestIDFromContext(ctx) + + session, err := h.authn.BeginAuth(reqID) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("decode session cookie: %w", err)) + } + + redirectURL, err := session.GetAuthURL() + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("get redirection URL: %w", err)) + } + + sessionCookie, err := h.cookies.Encode(sessionName, &authSession{ + State: reqID, + Session: session.Marshal(), + }) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("encode session cookie: %w", err)) + } + + return &web.LoginGoogleResult{ + RedirectURL: redirectURL, + SessionCookie: sessionCookie, + }, nil +} + +func (h *Handler) LoginGoogleCallback(ctx context.Context, req *web.LoginGoogleCallbackPayload) (*web.LoginGoogleCallbackResult, error) { + var auth *authSession + if err := h.cookies.Decode(sessionName, req.SessionCookie, &auth); err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("decode session cookie: %w", err)) + } + + if req.State != auth.State { + return nil, web.MakeUnauthorized(errors.New("invalid state")) + } + + session, err := h.authn.UnmarshalSession(auth.Session) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("unmarshal session data: %w", err)) + } + + params := url.Values{} + params.Set("code", req.Code) + + accessToken, err := session.Authorize(h.authn, params) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("authorize session: %w", err)) + } + + res, err := h.api.AuthToken(ctx, &api.AuthTokenPayload{ + Provider: "google", + AccessToken: accessToken, + }) + if err != nil { + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + switch gerr.Name { + case apiv1.ErrCodeIncompleteAuthInfo: + return nil, web.MakeUnauthorized(errors.New("authentication failed")) + } + } + return nil, web.MakeUnauthorized(errors.New("failed to exchange auth token")) + } + + sessionCookie, err := h.cookies.Encode(sessionName, &authSession{ + Token: res.Token, + }) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("encode session cookie: %w", err)) + } + + return &web.LoginGoogleCallbackResult{ + RedirectURL: "/", + SessionCookie: sessionCookie, + }, nil +} + +func (h *Handler) Logout(ctx context.Context, req *web.LogoutPayload) (*web.LogoutResult, error) { + return &web.LogoutResult{ + RedirectURL: "/", + SessionCookie: "", + }, nil +} + +func (h *Handler) SessionToken(ctx context.Context, req *web.SessionTokenPayload) (*web.SessionTokenResult, error) { + var auth *authSession + if err := h.cookies.Decode(sessionName, req.SessionCookie, &auth); err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("decode session cookie: %w", err)) + } + + return &web.SessionTokenResult{ + Token: auth.Token, + }, nil +} diff --git a/app/internal/handler/web/handler.go b/app/internal/handler/web/handler.go new file mode 100644 index 0000000..9fb07d6 --- /dev/null +++ b/app/internal/handler/web/handler.go @@ -0,0 +1,60 @@ +package web + +import ( + "context" + "embed" + "fmt" + "html/template" + + "github.com/alexliesenfeld/health" + "github.com/gorilla/securecookie" + "github.com/markbates/goth" + + "github.com/jace-ys/countup/api/v1/gen/api" + "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 { + authn goth.Provider + cookies *securecookie.SecureCookie + api *api.Client + tmpls *template.Template +} + +func NewHandler(authn goth.Provider, cookies *securecookie.SecureCookie, api *api.Client) (*Handler, error) { + tmpls, err := template.New("tmpls").ParseFS(templateFS, "**/*.html") + if err != nil { + return nil, fmt.Errorf("parse templates: %w", err) + } + + return &Handler{ + authn: authn, + cookies: cookies.MaxAge(86400), + api: api, + 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 + }, + }, + } +} diff --git a/app/internal/handler/web/pages.go b/app/internal/handler/web/pages.go new file mode 100644 index 0000000..6322dfe --- /dev/null +++ b/app/internal/handler/web/pages.go @@ -0,0 +1,39 @@ +package web + +import ( + "bytes" + "context" + "fmt" +) + +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) +} + +type renderData struct { + Data any +} + +func (h *Handler) render(page string, data any) ([]byte, error) { + buf := &bytes.Buffer{} + if err := h.tmpls.ExecuteTemplate(buf, page, renderData{data}); err != nil { + return nil, fmt.Errorf("render: %w", err) + } + return buf.Bytes(), nil +} 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 @@ + + + +
+ + ++