chore: good enough

This commit is contained in:
Tine 2024-02-02 15:39:13 +01:00
parent 568f001ede
commit bf16559d0d
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
30 changed files with 585 additions and 973 deletions

View file

@ -7,6 +7,7 @@ RUN go mod download
# Development
FROM base as dev
RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.1.0
RUN curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
CMD ["air"]
@ -16,5 +17,6 @@ RUN go build cmd/server.go
# Production
FROM gcr.io/distroless/static-debian12 as prod
COPY --from=build /app/server /
COPY --from=build /app/server /app/
COPY --from=build /app/migrations /app/migrations
ENTRYPOINT ["/server"]

View file

@ -3,11 +3,13 @@
### Development
Tools required:
* Justfile
* [Just](https://github.com/casey/just)
* Docker and Docker Compose
```sh
just run
# Api is available at http://localhost:1234
# Swagger is available at http://localhost:1235
curl -v localhost:1234/healthz
```

View file

@ -8,7 +8,7 @@ info:
description: >
Example OpenAPI Golang server.
servers:
- url: http://localhost:1323/api/v1
- url: http://localhost:1234
description: Local server
tags:
- name: health
@ -131,38 +131,6 @@ paths:
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/UnexpectedError'
/users/{id}/group:
put:
description: Update user group by id
tags:
- users
parameters:
- name: id
in: path
required: true
schema:
$ref: '#/components/schemas/Id'
requestBody:
description: Group object
required: true
content:
application/json:
schema:
type: object
properties:
group:
$ref: '#/components/schemas/Id'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/UnexpectedError'
/groups:
get:
description: Get all groups
@ -317,7 +285,7 @@ components:
- id
- name
- email
- group
- group_id
properties:
id:
$ref: '#/components/schemas/Id'
@ -327,13 +295,14 @@ components:
email:
type: string
example: john@example.com
group:
$ref: '#/components/schemas/Group'
group_id:
$ref: '#/components/schemas/Id'
UserUpdate:
type: object
required:
- name
- email
- group_id
properties:
name:
type: string
@ -341,22 +310,24 @@ components:
email:
type: string
example: john@example.com
group_id:
$ref: '#/components/schemas/Id'
Group:
type: object
required:
- id
- name
- users
- user_ids
properties:
id:
$ref: '#/components/schemas/Id'
name:
type: string
example: admins
users:
user_ids:
type: array
items:
$ref: '#/components/schemas/User'
$ref: '#/components/schemas/Id'
GroupUpdate:
type: object
required:

View file

@ -1,23 +1,68 @@
package main
import (
"fmt"
"log"
"net/http"
"github.com/mentos1386/golang-rest-example/pkg/api"
"github.com/mentos1386/golang-rest-example/pkg/openapi"
"github.com/ogen-go/ogen/middleware"
"github.com/rs/cors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
service := &api.ApiService{}
func Logging(logger *zap.Logger) middleware.Middleware {
return func(
req middleware.Request,
next func(req middleware.Request) (middleware.Response, error),
) (middleware.Response, error) {
logger := logger.With(
zap.String("operation", req.OperationName),
zap.String("operationId", req.OperationID),
)
logger.Info("Handling request")
resp, err := next(req)
if err != nil {
logger.Error("Fail", zap.Error(err))
} else {
var fields []zapcore.Field
// Some response types may have a status code.
// ogen provides a getter for it.
//
// You can write your own interface to match any response type.
if tresp, ok := resp.Type.(interface{ GetStatusCode() int }); ok {
fields = []zapcore.Field{
zap.Int("status_code", tresp.GetStatusCode()),
}
}
logger.Info("Success", fields...)
}
return resp, err
}
}
srv, err := openapi.NewServer(service)
func main() {
service := api.NewApiService()
logger, _ := zap.NewDevelopment()
srv, err := openapi.NewServer(service, openapi.WithMiddleware(Logging(logger)))
if err != nil {
log.Fatal(err)
}
log.Println("Starting server on :1234")
if err := http.ListenAndServe(":1234", srv); err != nil {
address := fmt.Sprintf(":%d", service.Config.Port)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedHeaders: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
})
logger.Info("Starting server", zap.String("address", address))
if err := http.ListenAndServe(address, c.Handler(srv)); err != nil {
log.Fatal(err)
}
}

View file

@ -4,7 +4,14 @@ services:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
app:
build:
@ -17,5 +24,20 @@ services:
- 1234:1234
volumes:
- .:/app
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgresql://postgres:example@db:5432/postgres
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres?sslmode=disable
- PORT=1234
swagger:
image: swaggerapi/swagger-ui
ports:
- 1235:8080
environment:
- SWAGGER_JSON=/api/openapi.yaml
volumes:
- ./api:/api
depends_on:
- app

9
go.mod
View file

@ -5,11 +5,16 @@ go 1.21.6
require (
github.com/go-faster/errors v0.7.1
github.com/go-faster/jx v1.1.0
github.com/golang-migrate/migrate/v4 v4.17.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/lib/pq v1.10.9
github.com/ogen-go/ogen v0.81.2
github.com/rs/cors v1.10.1
go.opentelemetry.io/otel v1.22.0
go.opentelemetry.io/otel/metric v1.22.0
go.opentelemetry.io/otel/trace v1.22.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.26.0
)
require (
@ -20,10 +25,12 @@ require (
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/segmentio/asm v1.2.0 // indirect
go.uber.org/zap v1.26.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.6.0 // indirect

48
go.sum
View file

@ -1,7 +1,22 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
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/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M=
github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@ -17,27 +32,54 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU=
github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ogen-go/ogen v0.81.2 h1:Dj5vSgC/1oqLE5t0T5qd4ARgsKTupJWsh3rW9/C7Lvk=
github.com/ogen-go/ogen v0.81.2/go.mod h1:10Ch7SIzBMSLB8TVEt8KclMKkRyJ5qCh4Cfs0pdeoh8=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
@ -46,6 +88,8 @@ go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@ -54,6 +98,8 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
@ -64,6 +110,8 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
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=

View file

@ -5,16 +5,29 @@ _default:
# Run the app in development mode
run:
docker compose up --build app
docker compose up --build app swagger
delete:
docker compose down
# Generate OpenAPI files
gen:
docker compose run --rm app \
docker compose run --build --rm app \
go generate ./...
# Create a new migration file
migration-create name:
@docker compose run --build --rm app \
migrate create -ext sql -dir migrations -seq {{name}}
# Build production image
build:
docker build \
--build-arg GO_VERSION=$(GO_VERSION) \
-t golang-rest-example \
.
# Run pgcli to connect to the database
db-cli:
docker compose exec db psql -U postgres -d postgres

View file

View file

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS groups(
id serial PRIMARY KEY,
name VARCHAR (300) UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS users(
id serial PRIMARY KEY,
email VARCHAR (300) UNIQUE NOT NULL,
name VARCHAR (100) UNIQUE NOT NULL,
group_id INT,
CONSTRAINT fk_users_groups_group_id
FOREIGN KEY(group_id)
REFERENCES groups(id)
);

View file

@ -1,11 +1,70 @@
package api
import (
"database/sql"
"time"
migrate "github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/lib/pq"
"github.com/mentos1386/golang-rest-example/pkg/config"
"github.com/mentos1386/golang-rest-example/pkg/openapi"
"go.uber.org/zap"
)
type ApiService struct {
logger *zap.Logger
db *sql.DB
groups []*openapi.Group
Config *config.Config
openapi.UnimplementedHandler
}
func NewApiService() *ApiService {
logger, _ := zap.NewDevelopment()
config, err := config.NewConfig()
if err != nil {
logger.Fatal("Failed to load the config", zap.Error(err))
}
db, err := sql.Open("postgres", config.DatabaseURL)
if err != nil {
logger.Fatal("Failed to connect to the database", zap.Error(err))
}
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(time.Hour)
err = db.Ping()
if err != nil {
logger.Fatal("Failed to ping the database", zap.Error(err))
}
logger.Info("Connected to the database")
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
logger.Fatal("Failed to create the driver", zap.Error(err))
}
m, err := migrate.NewWithDatabaseInstance(
"file:///app/migrations",
"postgres", driver)
if err != nil {
logger.Fatal("Failed to create the migration", zap.Error(err))
}
err = m.Up()
if err != nil && err != migrate.ErrNoChange {
logger.Fatal("Failed to apply the migrations", zap.Error(err))
}
logger.Info("Migrations applied")
return &ApiService{
logger: logger,
db: db,
Config: config,
}
}

View file

@ -2,14 +2,139 @@ package api
import (
"context"
"database/sql"
"github.com/mentos1386/golang-rest-example/pkg/openapi"
)
func (u *ApiService) GroupsGet(ctx context.Context) ([]openapi.Group, error) {
var groups []openapi.Group
for _, group := range u.groups {
groups = append(groups, *group)
type Group struct {
ID int64
Name string
UserIds []int64
}
func (u *ApiService) getUsersForGroupId(id openapi.ID) ([]openapi.ID, error) {
rows, err := u.db.Query("SELECT id FROM users WHERE group_id = $1", id)
if err != nil {
return nil, err
}
groups = append(groups, openapi.Group{ID: openapi.ID(1), Name: "Admins"})
defer rows.Close()
var userIds []openapi.ID
for rows.Next() {
var userId int64
err = rows.Scan(&userId)
if err != nil {
return nil, err
}
userIds = append(userIds, openapi.ID(userId))
}
return userIds, nil
}
func (u *ApiService) GroupsGet(ctx context.Context) ([]openapi.Group, error) {
rows, err := u.db.Query("SELECT * FROM groups")
if err != nil {
return nil, err
}
defer rows.Close()
var groups []openapi.Group
for rows.Next() {
var group Group
err = rows.Scan(&group.ID, &group.Name)
if err != nil {
return nil, err
}
userIds, err := u.getUsersForGroupId(openapi.ID(group.ID))
if err != nil {
return nil, err
}
groups = append(groups, openapi.Group{
ID: openapi.ID(group.ID),
Name: group.Name,
UserIds: userIds,
})
}
return groups, nil
}
func (u *ApiService) GroupsPost(ctx context.Context, group *openapi.GroupUpdate) (*openapi.Group, error) {
row := u.db.QueryRow("INSERT INTO groups (name) VALUES ($1) RETURNING id", group.Name)
var id int64
err := row.Scan(&id)
if err != nil {
return nil, err
}
// Just created group has no users
var userIds []openapi.ID
return &openapi.Group{
ID: openapi.ID(id),
Name: group.Name,
UserIds: userIds,
}, nil
}
func (u *ApiService) GroupsIDGet(ctx context.Context, params openapi.GroupsIDGetParams) (openapi.GroupsIDGetRes, error) {
row := u.db.QueryRow("SELECT * FROM groups WHERE id = $1", params.ID)
var group Group
err := row.Scan(&group.ID, &group.Name)
if err != nil {
if err == sql.ErrNoRows {
return &openapi.Error{Message: "Group not found", Code: 404}, nil
}
return nil, err
}
userIds, err := u.getUsersForGroupId(openapi.ID(group.ID))
if err != nil {
return nil, err
}
return &openapi.Group{
ID: openapi.ID(group.ID),
Name: group.Name,
UserIds: userIds,
}, nil
}
func (u *ApiService) GroupsIDPut(ctx context.Context, group *openapi.GroupUpdate, params openapi.GroupsIDPutParams) (openapi.GroupsIDPutRes, error) {
res, err := u.db.Exec("UPDATE groups SET name = $1 WHERE id = $2", group.Name, params.ID)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return &openapi.Error{Message: "Group not found", Code: 404}, nil
}
userIds, err := u.getUsersForGroupId(params.ID)
if err != nil {
return nil, err
}
return &openapi.Group{
ID: params.ID,
Name: group.Name,
UserIds: userIds,
}, nil
}
func (u *ApiService) GroupsIDDelete(ctx context.Context, params openapi.GroupsIDDeleteParams) (openapi.GroupsIDDeleteRes, error) {
res, err := u.db.Exec("DELETE FROM groups WHERE id = $1", params.ID)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return &openapi.Error{Message: "Group not found", Code: 404}, nil
}
return &openapi.Ok{Message: "OK"}, nil
}

15
pkg/api/healthz.go Normal file
View file

@ -0,0 +1,15 @@
package api
import (
"context"
"github.com/mentos1386/golang-rest-example/pkg/openapi"
)
func (u *ApiService) HealthzGet(ctx context.Context) (*openapi.Ok, error) {
err := u.db.Ping()
if err != nil {
return nil, err
}
return &openapi.Ok{Message: "OK"}, nil
}

View file

@ -1,3 +1,106 @@
package api
type UserServer struct{}
import (
"context"
"database/sql"
"github.com/mentos1386/golang-rest-example/pkg/openapi"
)
type User struct {
ID int64
Name string
Email string
GroupID int64
}
func (u *ApiService) UsersGet(ctx context.Context) ([]openapi.User, error) {
rows, err := u.db.Query("SELECT * FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []openapi.User
for rows.Next() {
var user User
err = rows.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
users = append(users, openapi.User{
ID: openapi.ID(user.ID),
Name: user.Name,
})
}
return users, nil
}
func (u *ApiService) UsersPost(ctx context.Context, user *openapi.UserUpdate) (*openapi.User, error) {
row := u.db.QueryRow("INSERT INTO users (name, email, group_id) VALUES ($1, $2, $3) RETURNING id", user.Name, user.Email, user.GroupID)
var id int64
err := row.Scan(&id)
if err != nil {
return nil, err
}
return &openapi.User{
ID: openapi.ID(id),
Name: user.Name,
Email: user.Email,
GroupID: user.GroupID,
}, nil
}
func (u *ApiService) UsersIDGet(ctx context.Context, params openapi.UsersIDGetParams) (openapi.UsersIDGetRes, error) {
row := u.db.QueryRow("SELECT * FROM users WHERE id = $1", params.ID)
var user User
err := row.Scan(&user.ID, &user.Name)
if err != nil {
if err == sql.ErrNoRows {
return &openapi.Error{Message: "User not found", Code: 404}, nil
}
return nil, err
}
return &openapi.User{
ID: openapi.ID(user.ID),
Name: user.Name,
Email: user.Email,
GroupID: openapi.ID(user.GroupID),
}, nil
}
func (u *ApiService) UsersIDPut(ctx context.Context, user *openapi.UserUpdate, params openapi.UsersIDPutParams) (openapi.UsersIDPutRes, error) {
res, err := u.db.Exec("UPDATE users SET name = $1, email = $2, group_id = $3 WHERE id = $4", user.Name, user.Email, user.GroupID, params.ID)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return &openapi.Error{Message: "User not found", Code: 404}, nil
}
return &openapi.User{
ID: params.ID,
Name: user.Name,
Email: user.Email,
GroupID: openapi.ID(user.GroupID),
}, nil
}
func (u *ApiService) UsersIDDelete(ctx context.Context, params openapi.UsersIDDeleteParams) (openapi.UsersIDDeleteRes, error) {
res, err := u.db.Exec("DELETE FROM users WHERE id = $1", params.ID)
if err != nil {
return nil, err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return &openapi.Error{Message: "User not found", Code: 404}, nil
}
return &openapi.Ok{Message: "OK"}, nil
}

19
pkg/config/config.go Normal file
View file

@ -0,0 +1,19 @@
package config
import (
"github.com/kelseyhightower/envconfig"
)
type Config struct {
Port int `envconfig:"PORT" default:"8080"`
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
}
func NewConfig() (*Config, error) {
var cfg Config
err := envconfig.Process("", &cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}

View file

@ -74,12 +74,6 @@ type Invoker interface {
//
// GET /users/{id}
UsersIDGet(ctx context.Context, params UsersIDGetParams) (UsersIDGetRes, error)
// UsersIDGroupPut invokes PUT /users/{id}/group operation.
//
// Update user group by id.
//
// PUT /users/{id}/group
UsersIDGroupPut(ctx context.Context, request *UsersIDGroupPutReq, params UsersIDGroupPutParams) (UsersIDGroupPutRes, error)
// UsersIDPut invokes PUT /users/{id} operation.
//
// Update user by id.
@ -894,102 +888,6 @@ func (c *Client) sendUsersIDGet(ctx context.Context, params UsersIDGetParams) (r
return result, nil
}
// UsersIDGroupPut invokes PUT /users/{id}/group operation.
//
// Update user group by id.
//
// PUT /users/{id}/group
func (c *Client) UsersIDGroupPut(ctx context.Context, request *UsersIDGroupPutReq, params UsersIDGroupPutParams) (UsersIDGroupPutRes, error) {
res, err := c.sendUsersIDGroupPut(ctx, request, params)
return res, err
}
func (c *Client) sendUsersIDGroupPut(ctx context.Context, request *UsersIDGroupPutReq, params UsersIDGroupPutParams) (res UsersIDGroupPutRes, err error) {
otelAttrs := []attribute.KeyValue{
semconv.HTTPMethodKey.String("PUT"),
semconv.HTTPRouteKey.String("/users/{id}/group"),
}
// Run stopwatch.
startTime := time.Now()
defer func() {
// Use floating point division here for higher precision (instead of Millisecond method).
elapsedDuration := time.Since(startTime)
c.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
// Start a span for this request.
ctx, span := c.cfg.Tracer.Start(ctx, "UsersIDGroupPut",
trace.WithAttributes(otelAttrs...),
clientSpanKind,
)
// Track stage for error reporting.
var stage string
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
span.End()
}()
stage = "BuildURL"
u := uri.Clone(c.requestURL(ctx))
var pathParts [3]string
pathParts[0] = "/users/"
{
// Encode "id" parameter.
e := uri.NewPathEncoder(uri.PathEncoderConfig{
Param: "id",
Style: uri.PathStyleSimple,
Explode: false,
})
if err := func() error {
if unwrapped := int64(params.ID); true {
return e.EncodeValue(conv.Int64ToString(unwrapped))
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "encode path")
}
encoded, err := e.Result()
if err != nil {
return res, errors.Wrap(err, "encode path")
}
pathParts[1] = encoded
}
pathParts[2] = "/group"
uri.AddPathParts(u, pathParts[:]...)
stage = "EncodeRequest"
r, err := ht.NewRequest(ctx, "PUT", u)
if err != nil {
return res, errors.Wrap(err, "create request")
}
if err := encodeUsersIDGroupPutRequest(request, r); err != nil {
return res, errors.Wrap(err, "encode request")
}
stage = "SendRequest"
resp, err := c.cfg.Client.Do(r)
if err != nil {
return res, errors.Wrap(err, "do request")
}
defer resp.Body.Close()
stage = "DecodeResponse"
result, err := decodeUsersIDGroupPutResponse(resp)
if err != nil {
return res, errors.Wrap(err, "decode response")
}
return result, nil
}
// UsersIDPut invokes PUT /users/{id} operation.
//
// Update user by id.

View file

@ -1019,137 +1019,6 @@ func (s *Server) handleUsersIDGetRequest(args [1]string, argsEscaped bool, w htt
}
}
// handleUsersIDGroupPutRequest handles PUT /users/{id}/group operation.
//
// Update user group by id.
//
// PUT /users/{id}/group
func (s *Server) handleUsersIDGroupPutRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) {
otelAttrs := []attribute.KeyValue{
semconv.HTTPMethodKey.String("PUT"),
semconv.HTTPRouteKey.String("/users/{id}/group"),
}
// Start a span for this request.
ctx, span := s.cfg.Tracer.Start(r.Context(), "UsersIDGroupPut",
trace.WithAttributes(otelAttrs...),
serverSpanKind,
)
defer span.End()
// Run stopwatch.
startTime := time.Now()
defer func() {
elapsedDuration := time.Since(startTime)
// Use floating point division here for higher precision (instead of Millisecond method).
s.duration.Record(ctx, float64(float64(elapsedDuration)/float64(time.Millisecond)), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
s.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
var (
recordError = func(stage string, err error) {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
s.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
err error
opErrContext = ogenerrors.OperationContext{
Name: "UsersIDGroupPut",
ID: "",
}
)
params, err := decodeUsersIDGroupPutParams(args, argsEscaped, r)
if err != nil {
err = &ogenerrors.DecodeParamsError{
OperationContext: opErrContext,
Err: err,
}
recordError("DecodeParams", err)
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
request, close, err := s.decodeUsersIDGroupPutRequest(r)
if err != nil {
err = &ogenerrors.DecodeRequestError{
OperationContext: opErrContext,
Err: err,
}
recordError("DecodeRequest", err)
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
defer func() {
if err := close(); err != nil {
recordError("CloseRequest", err)
}
}()
var response UsersIDGroupPutRes
if m := s.cfg.Middleware; m != nil {
mreq := middleware.Request{
Context: ctx,
OperationName: "UsersIDGroupPut",
OperationSummary: "",
OperationID: "",
Body: request,
Params: middleware.Parameters{
{
Name: "id",
In: "path",
}: params.ID,
},
Raw: r,
}
type (
Request = *UsersIDGroupPutReq
Params = UsersIDGroupPutParams
Response = UsersIDGroupPutRes
)
response, err = middleware.HookMiddleware[
Request,
Params,
Response,
](
m,
mreq,
unpackUsersIDGroupPutParams,
func(ctx context.Context, request Request, params Params) (response Response, err error) {
response, err = s.h.UsersIDGroupPut(ctx, request, params)
return response, err
},
)
} else {
response, err = s.h.UsersIDGroupPut(ctx, request, params)
}
if err != nil {
if errRes, ok := errors.Into[*ErrorStatusCode](err); ok {
if err := encodeErrorResponse(errRes, w, span); err != nil {
recordError("Internal", err)
}
return
}
if errors.Is(err, ht.ErrNotImplemented) {
s.cfg.ErrorHandler(ctx, w, r, err)
return
}
if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil {
recordError("Internal", err)
}
return
}
if err := encodeUsersIDGroupPutResponse(response, w, span); err != nil {
recordError("EncodeResponse", err)
if !errors.Is(err, ht.ErrInternalServerErrorResponse) {
s.cfg.ErrorHandler(ctx, w, r, err)
}
return
}
}
// handleUsersIDPutRequest handles PUT /users/{id} operation.
//
// Update user by id.

View file

@ -21,10 +21,6 @@ type UsersIDGetRes interface {
usersIDGetRes()
}
type UsersIDGroupPutRes interface {
usersIDGroupPutRes()
}
type UsersIDPutRes interface {
usersIDPutRes()
}

View file

@ -143,9 +143,9 @@ func (s *Group) encodeFields(e *jx.Encoder) {
e.Str(s.Name)
}
{
e.FieldStart("users")
e.FieldStart("user_ids")
e.ArrStart()
for _, elem := range s.Users {
for _, elem := range s.UserIds {
elem.Encode(e)
}
e.ArrEnd()
@ -155,7 +155,7 @@ func (s *Group) encodeFields(e *jx.Encoder) {
var jsonFieldsNameOfGroup = [3]string{
0: "id",
1: "name",
2: "users",
2: "user_ids",
}
// Decode decodes Group from json.
@ -189,23 +189,23 @@ func (s *Group) Decode(d *jx.Decoder) error {
}(); err != nil {
return errors.Wrap(err, "decode field \"name\"")
}
case "users":
case "user_ids":
requiredBitSet[0] |= 1 << 2
if err := func() error {
s.Users = make([]User, 0)
s.UserIds = make([]ID, 0)
if err := d.Arr(func(d *jx.Decoder) error {
var elem User
var elem ID
if err := elem.Decode(d); err != nil {
return err
}
s.Users = append(s.Users, elem)
s.UserIds = append(s.UserIds, elem)
return nil
}); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"users\"")
return errors.Wrap(err, "decode field \"user_ids\"")
}
default:
return d.Skip()
@ -495,39 +495,6 @@ func (s *Ok) UnmarshalJSON(data []byte) error {
return s.Decode(d)
}
// Encode encodes ID as json.
func (o OptID) Encode(e *jx.Encoder) {
if !o.Set {
return
}
o.Value.Encode(e)
}
// Decode decodes ID from json.
func (o *OptID) Decode(d *jx.Decoder) error {
if o == nil {
return errors.New("invalid: unable to decode OptID to nil")
}
o.Set = true
if err := o.Value.Decode(d); err != nil {
return err
}
return nil
}
// MarshalJSON implements stdjson.Marshaler.
func (s OptID) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *OptID) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}
// Encode implements json.Marshaler.
func (s *User) Encode(e *jx.Encoder) {
e.ObjStart()
@ -550,8 +517,8 @@ func (s *User) encodeFields(e *jx.Encoder) {
e.Str(s.Email)
}
{
e.FieldStart("group")
s.Group.Encode(e)
e.FieldStart("group_id")
s.GroupID.Encode(e)
}
}
@ -559,7 +526,7 @@ var jsonFieldsNameOfUser = [4]string{
0: "id",
1: "name",
2: "email",
3: "group",
3: "group_id",
}
// Decode decodes User from json.
@ -605,15 +572,15 @@ func (s *User) Decode(d *jx.Decoder) error {
}(); err != nil {
return errors.Wrap(err, "decode field \"email\"")
}
case "group":
case "group_id":
requiredBitSet[0] |= 1 << 3
if err := func() error {
if err := s.Group.Decode(d); err != nil {
if err := s.GroupID.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"group\"")
return errors.Wrap(err, "decode field \"group_id\"")
}
default:
return d.Skip()
@ -688,11 +655,16 @@ func (s *UserUpdate) encodeFields(e *jx.Encoder) {
e.FieldStart("email")
e.Str(s.Email)
}
{
e.FieldStart("group_id")
s.GroupID.Encode(e)
}
}
var jsonFieldsNameOfUserUpdate = [2]string{
var jsonFieldsNameOfUserUpdate = [3]string{
0: "name",
1: "email",
2: "group_id",
}
// Decode decodes UserUpdate from json.
@ -728,6 +700,16 @@ func (s *UserUpdate) Decode(d *jx.Decoder) error {
}(); err != nil {
return errors.Wrap(err, "decode field \"email\"")
}
case "group_id":
requiredBitSet[0] |= 1 << 2
if err := func() error {
if err := s.GroupID.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"group_id\"")
}
default:
return d.Skip()
}
@ -738,7 +720,7 @@ func (s *UserUpdate) Decode(d *jx.Decoder) error {
// Validate required fields.
var failures []validate.FieldError
for i, mask := range [1]uint8{
0b00000011,
0b00000111,
} {
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
// Mask only required fields and check equality to mask using XOR.
@ -783,66 +765,3 @@ func (s *UserUpdate) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}
// Encode implements json.Marshaler.
func (s *UsersIDGroupPutReq) Encode(e *jx.Encoder) {
e.ObjStart()
s.encodeFields(e)
e.ObjEnd()
}
// encodeFields encodes fields.
func (s *UsersIDGroupPutReq) encodeFields(e *jx.Encoder) {
{
if s.Group.Set {
e.FieldStart("group")
s.Group.Encode(e)
}
}
}
var jsonFieldsNameOfUsersIDGroupPutReq = [1]string{
0: "group",
}
// Decode decodes UsersIDGroupPutReq from json.
func (s *UsersIDGroupPutReq) Decode(d *jx.Decoder) error {
if s == nil {
return errors.New("invalid: unable to decode UsersIDGroupPutReq to nil")
}
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
switch string(k) {
case "group":
if err := func() error {
s.Group.Reset()
if err := s.Group.Decode(d); err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"group\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode UsersIDGroupPutReq")
}
return nil
}
// MarshalJSON implements stdjson.Marshaler.
func (s *UsersIDGroupPutReq) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *UsersIDGroupPutReq) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}

View file

@ -375,78 +375,6 @@ func decodeUsersIDGetParams(args [1]string, argsEscaped bool, r *http.Request) (
return params, nil
}
// UsersIDGroupPutParams is parameters of PUT /users/{id}/group operation.
type UsersIDGroupPutParams struct {
ID ID
}
func unpackUsersIDGroupPutParams(packed middleware.Parameters) (params UsersIDGroupPutParams) {
{
key := middleware.ParameterKey{
Name: "id",
In: "path",
}
params.ID = packed[key].(ID)
}
return params
}
func decodeUsersIDGroupPutParams(args [1]string, argsEscaped bool, r *http.Request) (params UsersIDGroupPutParams, _ error) {
// Decode path: id.
if err := func() error {
param := args[0]
if argsEscaped {
unescaped, err := url.PathUnescape(args[0])
if err != nil {
return errors.Wrap(err, "unescape path")
}
param = unescaped
}
if len(param) > 0 {
d := uri.NewPathDecoder(uri.PathDecoderConfig{
Param: "id",
Value: param,
Style: uri.PathStyleSimple,
Explode: false,
})
if err := func() error {
var paramsDotIDVal int64
if err := func() error {
val, err := d.DecodeValue()
if err != nil {
return err
}
c, err := conv.ToInt64(val)
if err != nil {
return err
}
paramsDotIDVal = c
return nil
}(); err != nil {
return err
}
params.ID = ID(paramsDotIDVal)
return nil
}(); err != nil {
return err
}
} else {
return validate.ErrFieldRequired
}
return nil
}(); err != nil {
return params, &ogenerrors.DecodeParamError{
Name: "id",
In: "path",
Err: err,
}
}
return params, nil
}
// UsersIDPutParams is parameters of PUT /users/{id} operation.
type UsersIDPutParams struct {
ID ID

View file

@ -141,69 +141,6 @@ func (s *Server) decodeGroupsPostRequest(r *http.Request) (
}
}
func (s *Server) decodeUsersIDGroupPutRequest(r *http.Request) (
req *UsersIDGroupPutReq,
close func() error,
rerr error,
) {
var closers []func() error
close = func() error {
var merr error
// Close in reverse order, to match defer behavior.
for i := len(closers) - 1; i >= 0; i-- {
c := closers[i]
merr = multierr.Append(merr, c())
}
return merr
}
defer func() {
if rerr != nil {
rerr = multierr.Append(rerr, close())
}
}()
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return req, close, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
if r.ContentLength == 0 {
return req, close, validate.ErrBodyRequired
}
buf, err := io.ReadAll(r.Body)
if err != nil {
return req, close, err
}
if len(buf) == 0 {
return req, close, validate.ErrBodyRequired
}
d := jx.DecodeBytes(buf)
var request UsersIDGroupPutReq
if err := func() error {
if err := request.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return req, close, err
}
return &request, close, nil
default:
return req, close, validate.InvalidContentType(ct)
}
}
func (s *Server) decodeUsersIDPutRequest(r *http.Request) (
req *UserUpdate,
close func() error,

View file

@ -39,20 +39,6 @@ func encodeGroupsPostRequest(
return nil
}
func encodeUsersIDGroupPutRequest(
req *UsersIDGroupPutReq,
r *http.Request,
) error {
const contentType = "application/json"
e := new(jx.Encoder)
{
req.Encode(e)
}
encoded := e.Bytes()
ht.SetBody(r, bytes.NewReader(encoded), contentType)
return nil
}
func encodeUsersIDPutRequest(
req *UserUpdate,
r *http.Request,

View file

@ -725,23 +725,6 @@ func decodeUsersGetResponse(resp *http.Response) (res []User, _ error) {
if response == nil {
return errors.New("nil is invalid value")
}
var failures []validate.FieldError
for i, elem := range response {
if err := func() error {
if err := elem.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: fmt.Sprintf("[%d]", i),
Error: err,
})
}
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
@ -947,142 +930,6 @@ func decodeUsersIDGetResponse(resp *http.Response) (res UsersIDGetRes, _ error)
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
case 404:
// Code 404.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeUsersIDGroupPutResponse(resp *http.Response) (res UsersIDGroupPutRes, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response User
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
@ -1201,15 +1048,6 @@ func decodeUsersIDPutResponse(resp *http.Response) (res UsersIDPutRes, _ error)
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
@ -1328,15 +1166,6 @@ func decodeUsersPostResponse(resp *http.Response) (res *User, _ error) {
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)

View file

@ -242,39 +242,6 @@ func encodeUsersIDGetResponse(response UsersIDGetRes, w http.ResponseWriter, spa
}
}
func encodeUsersIDGroupPutResponse(response UsersIDGroupPutRes, w http.ResponseWriter, span trace.Span) error {
switch response := response.(type) {
case *User:
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
span.SetStatus(codes.Ok, http.StatusText(200))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
case *Error:
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(404)
span.SetStatus(codes.Error, http.StatusText(404))
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
default:
return errors.Errorf("unexpected response type: %T", response)
}
}
func encodeUsersIDPutResponse(response UsersIDPutRes, w http.ResponseWriter, span trace.Span) error {
switch response := response.(type) {
case *User:

View file

@ -172,15 +172,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// Param: "id"
// Match until "/"
idx := strings.IndexByte(elem, '/')
if idx < 0 {
idx = len(elem)
}
args[0] = elem[:idx]
elem = elem[idx:]
// Leaf parameter
args[0] = elem
elem = ""
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "DELETE":
s.handleUsersIDDeleteRequest([1]string{
@ -200,31 +197,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
switch elem[0] {
case '/': // Prefix: "/group"
origElem := elem
if l := len("/group"); len(elem) >= l && elem[0:l] == "/group" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "PUT":
s.handleUsersIDGroupPutRequest([1]string{
args[0],
}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, "PUT")
}
return
}
elem = origElem
}
elem = origElem
}
@ -472,17 +444,14 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
}
// Param: "id"
// Match until "/"
idx := strings.IndexByte(elem, '/')
if idx < 0 {
idx = len(elem)
}
args[0] = elem[:idx]
elem = elem[idx:]
// Leaf parameter
args[0] = elem
elem = ""
if len(elem) == 0 {
switch method {
case "DELETE":
// Leaf: UsersIDDelete
r.name = "UsersIDDelete"
r.summary = ""
r.operationID = ""
@ -491,6 +460,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.count = 1
return r, true
case "GET":
// Leaf: UsersIDGet
r.name = "UsersIDGet"
r.summary = ""
r.operationID = ""
@ -499,6 +469,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
r.count = 1
return r, true
case "PUT":
// Leaf: UsersIDPut
r.name = "UsersIDPut"
r.summary = ""
r.operationID = ""
@ -510,33 +481,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
return
}
}
switch elem[0] {
case '/': // Prefix: "/group"
origElem := elem
if l := len("/group"); len(elem) >= l && elem[0:l] == "/group" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
switch method {
case "PUT":
// Leaf: UsersIDGroupPut
r.name = "UsersIDGroupPut"
r.summary = ""
r.operationID = ""
r.pathPattern = "/users/{id}/group"
r.args = args
r.count = 1
return r, true
default:
return
}
}
elem = origElem
}
elem = origElem
}

View file

@ -36,13 +36,12 @@ func (s *Error) SetCode(val int) {
s.Code = val
}
func (*Error) groupsIDDeleteRes() {}
func (*Error) groupsIDGetRes() {}
func (*Error) groupsIDPutRes() {}
func (*Error) usersIDDeleteRes() {}
func (*Error) usersIDGetRes() {}
func (*Error) usersIDGroupPutRes() {}
func (*Error) usersIDPutRes() {}
func (*Error) groupsIDDeleteRes() {}
func (*Error) groupsIDGetRes() {}
func (*Error) groupsIDPutRes() {}
func (*Error) usersIDDeleteRes() {}
func (*Error) usersIDGetRes() {}
func (*Error) usersIDPutRes() {}
// ErrorStatusCode wraps Error with StatusCode.
type ErrorStatusCode struct {
@ -72,9 +71,9 @@ func (s *ErrorStatusCode) SetResponse(val Error) {
// Ref: #/components/schemas/Group
type Group struct {
ID ID `json:"id"`
Name string `json:"name"`
Users []User `json:"users"`
ID ID `json:"id"`
Name string `json:"name"`
UserIds []ID `json:"user_ids"`
}
// GetID returns the value of ID.
@ -87,9 +86,9 @@ func (s *Group) GetName() string {
return s.Name
}
// GetUsers returns the value of Users.
func (s *Group) GetUsers() []User {
return s.Users
// GetUserIds returns the value of UserIds.
func (s *Group) GetUserIds() []ID {
return s.UserIds
}
// SetID sets the value of ID.
@ -102,9 +101,9 @@ func (s *Group) SetName(val string) {
s.Name = val
}
// SetUsers sets the value of Users.
func (s *Group) SetUsers(val []User) {
s.Users = val
// SetUserIds sets the value of UserIds.
func (s *Group) SetUserIds(val []ID) {
s.UserIds = val
}
func (*Group) groupsIDGetRes() {}
@ -145,58 +144,12 @@ func (s *Ok) SetMessage(val string) {
func (*Ok) groupsIDDeleteRes() {}
func (*Ok) usersIDDeleteRes() {}
// NewOptID returns new OptID with value set to v.
func NewOptID(v ID) OptID {
return OptID{
Value: v,
Set: true,
}
}
// OptID is optional ID.
type OptID struct {
Value ID
Set bool
}
// IsSet returns true if OptID was set.
func (o OptID) IsSet() bool { return o.Set }
// Reset unsets value.
func (o *OptID) Reset() {
var v ID
o.Value = v
o.Set = false
}
// SetTo sets value to v.
func (o *OptID) SetTo(v ID) {
o.Set = true
o.Value = v
}
// Get returns value and boolean that denotes whether value was set.
func (o OptID) Get() (v ID, ok bool) {
if !o.Set {
return v, false
}
return o.Value, true
}
// Or returns value if set, or given parameter if does not.
func (o OptID) Or(d ID) ID {
if v, ok := o.Get(); ok {
return v
}
return d
}
// Ref: #/components/schemas/User
type User struct {
ID ID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Group Group `json:"group"`
ID ID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
GroupID ID `json:"group_id"`
}
// GetID returns the value of ID.
@ -214,9 +167,9 @@ func (s *User) GetEmail() string {
return s.Email
}
// GetGroup returns the value of Group.
func (s *User) GetGroup() Group {
return s.Group
// GetGroupID returns the value of GroupID.
func (s *User) GetGroupID() ID {
return s.GroupID
}
// SetID sets the value of ID.
@ -234,19 +187,19 @@ func (s *User) SetEmail(val string) {
s.Email = val
}
// SetGroup sets the value of Group.
func (s *User) SetGroup(val Group) {
s.Group = val
// SetGroupID sets the value of GroupID.
func (s *User) SetGroupID(val ID) {
s.GroupID = val
}
func (*User) usersIDGetRes() {}
func (*User) usersIDGroupPutRes() {}
func (*User) usersIDPutRes() {}
func (*User) usersIDGetRes() {}
func (*User) usersIDPutRes() {}
// Ref: #/components/schemas/UserUpdate
type UserUpdate struct {
Name string `json:"name"`
Email string `json:"email"`
Name string `json:"name"`
Email string `json:"email"`
GroupID ID `json:"group_id"`
}
// GetName returns the value of Name.
@ -259,6 +212,11 @@ func (s *UserUpdate) GetEmail() string {
return s.Email
}
// GetGroupID returns the value of GroupID.
func (s *UserUpdate) GetGroupID() ID {
return s.GroupID
}
// SetName sets the value of Name.
func (s *UserUpdate) SetName(val string) {
s.Name = val
@ -269,16 +227,7 @@ func (s *UserUpdate) SetEmail(val string) {
s.Email = val
}
type UsersIDGroupPutReq struct {
Group OptID `json:"group"`
}
// GetGroup returns the value of Group.
func (s *UsersIDGroupPutReq) GetGroup() OptID {
return s.Group
}
// SetGroup sets the value of Group.
func (s *UsersIDGroupPutReq) SetGroup(val OptID) {
s.Group = val
// SetGroupID sets the value of GroupID.
func (s *UserUpdate) SetGroupID(val ID) {
s.GroupID = val
}

View file

@ -60,12 +60,6 @@ type Handler interface {
//
// GET /users/{id}
UsersIDGet(ctx context.Context, params UsersIDGetParams) (UsersIDGetRes, error)
// UsersIDGroupPut implements PUT /users/{id}/group operation.
//
// Update user group by id.
//
// PUT /users/{id}/group
UsersIDGroupPut(ctx context.Context, req *UsersIDGroupPutReq, params UsersIDGroupPutParams) (UsersIDGroupPutRes, error)
// UsersIDPut implements PUT /users/{id} operation.
//
// Update user by id.

View file

@ -92,15 +92,6 @@ func (UnimplementedHandler) UsersIDGet(ctx context.Context, params UsersIDGetPar
return r, ht.ErrNotImplemented
}
// UsersIDGroupPut implements PUT /users/{id}/group operation.
//
// Update user group by id.
//
// PUT /users/{id}/group
func (UnimplementedHandler) UsersIDGroupPut(ctx context.Context, req *UsersIDGroupPutReq, params UsersIDGroupPutParams) (r UsersIDGroupPutRes, _ error) {
return r, ht.ErrNotImplemented
}
// UsersIDPut implements PUT /users/{id} operation.
//
// Update user by id.

View file

@ -3,8 +3,6 @@
package openapi
import (
"fmt"
"github.com/go-faster/errors"
"github.com/ogen-go/ogen/validate"
@ -17,53 +15,13 @@ func (s *Group) Validate() error {
var failures []validate.FieldError
if err := func() error {
if s.Users == nil {
if s.UserIds == nil {
return errors.New("nil is invalid value")
}
var failures []validate.FieldError
for i, elem := range s.Users {
if err := func() error {
if err := elem.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: fmt.Sprintf("[%d]", i),
Error: err,
})
}
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "users",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *User) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if err := s.Group.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "group",
Name: "user_ids",
Error: err,
})
}

1
tmp/build-errors.log Normal file
View file

@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1