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. Go-Swagger
    in production
    wins and pitfalls

    View Slide

  2. Ilya Kaznacheev
    Remote Backend SWE
    Founder of Golang Voronezh
    Host of Z-Namespace podcast
    Organizer of conference and meetups
    Coffee geek

    View Slide

  3. what swagger is?

    View Slide

  4. View Slide

  5. SOAP
    JSON-PRC
    GraphQL
    gRPC
    OData
    REST

    View Slide

  6. Representational state transfer (REST)
    is a software architectural style that defines
    a set of constraints to be used
    for creating Web services
    Wikipedia

    View Slide

  7. View Slide

  8. 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

    View Slide

  9. View Slide

  10. View Slide

  11. why do we use swagger?

    View Slide

  12. my team trying to sync API changes...

    View Slide

  13. View Slide

  14. go-swagger

    View Slide

  15. code generation
    swagger generate server -t internal/api --exclude-main

    View Slide

  16. generated code structure
    internal/api
    ├ models
    │ └ ...
    └ restapi
    ├ operations
    │ └ ...
    ├ configure_.go
    ├ doc.go
    ├ embedded_spec.go
    └ server.go

    View Slide

  17. our code generation
    rm -rf internal/api && mkdir -p internal/api
    swagger generate server -t internal/api --exclude-main
    go mod tidy

    View Slide

  18. ?
    and we're all set

    View Slide

  19. NO

    View Slide

  20. there are some problems
    - go-swagger is a framework, not a library
    - plenty of generated types for everything
    - incompatible with popular http-libraries

    View Slide

  21. let’s fix ’em all!

    View Slide

  22. 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)
    })
    }

    View Slide

  23. simple middleware
    api := operations.NewSwaggerPetstoreAPI(swaggerSpec)
    api.InstrumentsGetMetricsHandler =
    instruments.GetMetricsHandlerFunc(MetricsHandler)
    api.AddMiddlewareFor("GET", "/metrics", SomeMiddleware)
    srv := restapi.NewServer(api)
    srv.Serve()

    View Slide

  24. 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()

    View Slide

  25. setup outside of configure_.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

    View Slide

  26. 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)
    }

    View Slide

  27. 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)
    }

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. extensions (tricks)
    x-omitempty
    x-nullable
    x-isnullable
    x-order
    x-go-custom-tag
    x-schemes
    x-go-name
    x-go-type
    x-go-json-string
    x-go-enum-ci

    View Slide

  31. 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()},
    }
    }

    View Slide

  32. 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)
    }

    View Slide

  33. 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}`,
    },
    }

    View Slide

  34. 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")
    })
    }

    View Slide

  35. View Slide

  36. helpful links
    json-schema.org/specification.html
    swagger.io/docs/specification/2-0
    goswagger.io
    bit.ly/go-swagger-in-production

    View Slide

  37. ilyakaznacheev

    View Slide