diff --git a/Dockerfile b/Dockerfile index d5959dc..eb717bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index ba567ac..312cb84 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/api/openapi.yaml b/api/openapi.yaml index 06b17ab..e94d550 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -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: diff --git a/cmd/server.go b/cmd/server.go index b222b0f..514a1f8 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -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) } } diff --git a/docker-compose.yaml b/docker-compose.yaml index a2ea3d2..969c76c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/go.mod b/go.mod index ae3afd7..7f68a3a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index db4b19b..da38d76 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/justfile b/justfile index 3d02bb9..b297315 100644 --- a/justfile +++ b/justfile @@ -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 diff --git a/migrations/000001_init.down.sql b/migrations/000001_init.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/migrations/000001_init.up.sql b/migrations/000001_init.up.sql new file mode 100644 index 0000000..44ed9b7 --- /dev/null +++ b/migrations/000001_init.up.sql @@ -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) +); diff --git a/pkg/api/api.go b/pkg/api/api.go index 1ff07a4..96e2120 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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, + } +} diff --git a/pkg/api/groups.go b/pkg/api/groups.go index bf0b483..d46cba3 100644 --- a/pkg/api/groups.go +++ b/pkg/api/groups.go @@ -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 +} diff --git a/pkg/api/healthz.go b/pkg/api/healthz.go new file mode 100644 index 0000000..891503a --- /dev/null +++ b/pkg/api/healthz.go @@ -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 +} diff --git a/pkg/api/users.go b/pkg/api/users.go index 86a91b8..385c4bf 100644 --- a/pkg/api/users.go +++ b/pkg/api/users.go @@ -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 +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..c9816e8 --- /dev/null +++ b/pkg/config/config.go @@ -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 +} diff --git a/pkg/openapi/oas_client_gen.go b/pkg/openapi/oas_client_gen.go index 740b6fa..bd1658a 100644 --- a/pkg/openapi/oas_client_gen.go +++ b/pkg/openapi/oas_client_gen.go @@ -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. diff --git a/pkg/openapi/oas_handlers_gen.go b/pkg/openapi/oas_handlers_gen.go index 79715bb..f5fecbc 100644 --- a/pkg/openapi/oas_handlers_gen.go +++ b/pkg/openapi/oas_handlers_gen.go @@ -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. diff --git a/pkg/openapi/oas_interfaces_gen.go b/pkg/openapi/oas_interfaces_gen.go index 4813ef1..5d3f660 100644 --- a/pkg/openapi/oas_interfaces_gen.go +++ b/pkg/openapi/oas_interfaces_gen.go @@ -21,10 +21,6 @@ type UsersIDGetRes interface { usersIDGetRes() } -type UsersIDGroupPutRes interface { - usersIDGroupPutRes() -} - type UsersIDPutRes interface { usersIDPutRes() } diff --git a/pkg/openapi/oas_json_gen.go b/pkg/openapi/oas_json_gen.go index 35d3f5c..f26d8d7 100644 --- a/pkg/openapi/oas_json_gen.go +++ b/pkg/openapi/oas_json_gen.go @@ -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) -} diff --git a/pkg/openapi/oas_parameters_gen.go b/pkg/openapi/oas_parameters_gen.go index 8eb5f1d..bc1c82a 100644 --- a/pkg/openapi/oas_parameters_gen.go +++ b/pkg/openapi/oas_parameters_gen.go @@ -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 diff --git a/pkg/openapi/oas_request_decoders_gen.go b/pkg/openapi/oas_request_decoders_gen.go index f324b99..84de828 100644 --- a/pkg/openapi/oas_request_decoders_gen.go +++ b/pkg/openapi/oas_request_decoders_gen.go @@ -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, diff --git a/pkg/openapi/oas_request_encoders_gen.go b/pkg/openapi/oas_request_encoders_gen.go index ee19408..ee325c5 100644 --- a/pkg/openapi/oas_request_encoders_gen.go +++ b/pkg/openapi/oas_request_encoders_gen.go @@ -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, diff --git a/pkg/openapi/oas_response_decoders_gen.go b/pkg/openapi/oas_response_decoders_gen.go index 3a42d70..8afb467 100644 --- a/pkg/openapi/oas_response_decoders_gen.go +++ b/pkg/openapi/oas_response_decoders_gen.go @@ -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) diff --git a/pkg/openapi/oas_response_encoders_gen.go b/pkg/openapi/oas_response_encoders_gen.go index e587f19..29c6e33 100644 --- a/pkg/openapi/oas_response_encoders_gen.go +++ b/pkg/openapi/oas_response_encoders_gen.go @@ -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: diff --git a/pkg/openapi/oas_router_gen.go b/pkg/openapi/oas_router_gen.go index 68627ce..6892c17 100644 --- a/pkg/openapi/oas_router_gen.go +++ b/pkg/openapi/oas_router_gen.go @@ -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 } diff --git a/pkg/openapi/oas_schemas_gen.go b/pkg/openapi/oas_schemas_gen.go index 9a7737d..28e4236 100644 --- a/pkg/openapi/oas_schemas_gen.go +++ b/pkg/openapi/oas_schemas_gen.go @@ -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 } diff --git a/pkg/openapi/oas_server_gen.go b/pkg/openapi/oas_server_gen.go index f2042fe..26f1b27 100644 --- a/pkg/openapi/oas_server_gen.go +++ b/pkg/openapi/oas_server_gen.go @@ -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. diff --git a/pkg/openapi/oas_unimplemented_gen.go b/pkg/openapi/oas_unimplemented_gen.go index edaa9e1..1b3ced7 100644 --- a/pkg/openapi/oas_unimplemented_gen.go +++ b/pkg/openapi/oas_unimplemented_gen.go @@ -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. diff --git a/pkg/openapi/oas_validators_gen.go b/pkg/openapi/oas_validators_gen.go index 1f2a673..22e48d8 100644 --- a/pkg/openapi/oas_validators_gen.go +++ b/pkg/openapi/oas_validators_gen.go @@ -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, }) } diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..00f1b0b --- /dev/null +++ b/tmp/build-errors.log @@ -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 \ No newline at end of file