Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go-Swagger in production

Go-Swagger in production

Ilya Kaznacheev

June 25, 2020
Tweet

More Decks by Ilya Kaznacheev

Other Decks in Programming

Transcript

  1. Ilya Kaznacheev Remote Backend SWE Founder of Golang Voronezh Host

    of Z-Namespace podcast Organizer of conference and meetups Coffee geek
  2. Representational state transfer (REST) is a software architectural style that

    defines a set of constraints to be used for creating Web services Wikipedia
  3. swagger: "2.0" info: title: Pet API version: "1.0.0" basePath: /api

    schemes: - http paths: /pets: get: summary: List all pets parameters: - name: limit in: query description: "How many items to return at one time" required: true type: integer responses: 200: description: an paged array of pets 400: description: unexpected error
  4. generated code structure internal/api ├ models │ └ ... └

    restapi ├ operations │ └ ... ├ configure_<your_service_name>.go ├ doc.go ├ embedded_spec.go └ server.go
  5. our code generation rm -rf internal/api && mkdir -p internal/api

    swagger generate server -t internal/api --exclude-main go mod tidy
  6. NO

  7. there are some problems - go-swagger is a framework, not

    a library - plenty of generated types for everything - incompatible with popular http-libraries
  8. serving net/http handlers type CustomResponder func(http.ResponseWriter, runtime.Producer) func (c CustomResponder)

    WriteResponse(w http.ResponseWriter, p runtime.Producer) { c(w, p) } func MetricsHandler(p instruments.GetMetricsParams) middleware.Responder { return CustomResponder(func(w http.ResponseWriter, _ runtime.Producer) { promhttp.Handler().ServeHTTP(w, p.HTTPRequest) }) }
  9. middleware with custom handler h := api.Serve(nil) r := chi.NewRouter()

    r.Use( middleware.Recoverer, ) r.With(AuthMiddleware).Group(func(r chi.Router) { r.Handle("/user/*", h) }) r.Mount("/", h) srv.ConfigureAPI() srv.SetHandler(r) srv.Serve()
  10. setup outside of configure_<your_service_name>.go api.Logger = log.Printf api.HTMLProducer = runtime.TextProducer()

    srv := restapi.NewServer(api) srv.EnabledListeners = []string{"http"} srv.Port = conf.HTTPPort srv.Host = conf.HTTPAddr
  11. custom method names /store/order/{orderId}/items: get: tags: - store summary: Find

    purchase order items parameters: - name: orderId in: path required: true type: integer func GetOrderItems( param store.GetStoreOrderOrderIDItemsParams, ) middleware.Responder { items, err := getOrderItems(param.OrderID) if err != nil { return store.NewGetStoreOrderOrderIDItemsNotFound() } res := &models.OrderItems{} // // fill resopnse // return store.NewGetStoreOrderOrderIDItemsOK(). WithPayload(res) }
  12. custom method names /store/order/{orderId}/items: get: tags: - store summary: Find

    purchase order items operationId: getOrderItems parameters: - name: orderId in: path required: true type: integer func GetOrderItems( param store.GetOrderItemsParams, ) middleware.Responder { items, err := getOrderItems(param.OrderID) if err != nil { return store.NewGetOrderItemsNotFound() } res := &models.OrderItems{} // // fill resopnse // return store.NewGetOrderItemsOK(). WithPayload(res) }
  13. validity checks OrderItems: type: object properties: message: type: string maximum:

    3 # swg/internal/api/models internal/api/models/order_items.go:45:55: cannot convert m.Message (type string) to type float64
  14. validity check cheat sheet numbers and integers - multipleOf -

    maximum - minimum - exclusiveMaximum - exclusiveMinimum strings - maxLength - minLength - pattern arrays - maxItems - minItems - uniqueItems - maxContains - minContains objects - maxProperties - minProperties - required - dependentRequired any type - type - enum - const
  15. Shortcuts Error: type: object required: - code - message properties:

    code: type: integer message: type: string type APIError struct { code int Payload *models.Error `json:"body,omitempty"` } func (e *APIError) WriteResponse( rw http.ResponseWriter, producer runtime.Producer) { rw.WriteHeader(e.code) producer.Produce(rw, e.Payload) } func RespondError(code int, err error) *APIError { return &APIError{ code: code, Payload: &models.Error{code, err.Error()}, } }
  16. unit tests func GetOrderByID(param store.GetOrderByIDParams) middleware.Responder { order := models.Order{

    ID: 123, PetID: 456, Quantity: 20, Status: "approved", } if param.OrderID != order.ID { return store.NewGetOrderByIDNotFound().WithPayload(&models.ErrorMessage{ Code: http.StatusNotFound, Message: http.StatusText(http.StatusNotFound), }) } return store.NewGetOrderByIDOK().WithPayload(&order) }
  17. unit tests tests := []struct { name string req store.GetOrderByIDParams

    code int want string }{ { name: "good test", req: store.GetOrderByIDParams{OrderID: 123}, code: 200, want: `{"id":123,"petId":456,"quantity":20,"status":"approved"}`, }, { name: "bad test", req: store.GetOrderByIDParams{OrderID: 456}, code: 404, want: `{"message":"Not Found", "code":404}`, }, }
  18. unit tests for _, tt := range tests { t.Run(tt.name,

    func(t *testing.T) { rr := httptest.NewRecorder() GetOrderByID(tt.req).WriteResponse(rr, runtime.JSONProducer()) assert.JSONEq(t, tt.want, rr.Body.String(), "wrong response body") assert.Equal(t, tt.code, rr.Code, "wrong response code") }) }