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 full-size slide

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

    View full-size slide

  3. what swagger is?

    View full-size slide

  4. SOAP
    JSON-PRC
    GraphQL
    gRPC
    OData
    REST

    View full-size slide

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

    View full-size slide

  6. 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 full-size slide

  7. why do we use swagger?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. ?
    and we're all set

    View full-size slide

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

    View full-size slide

  14. let’s fix ’em all!

    View full-size slide

  15. 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 full-size slide

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

    View full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

  27. 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 full-size slide

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

    View full-size slide

  29. ilyakaznacheev

    View full-size slide