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

Go-Swagger in production

Go-Swagger in production

Ilya Kaznacheev

May 21, 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. Golang Voronezh
    - ~30 active members
    - meetups
    - events for beginners
    - open & friendly

    View Slide

  4. what swagger is?

    View Slide

  5. View Slide

  6. SOAP
    JSON-PRC
    GraphQL
    gRPC
    OData
    REST

    View Slide

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

  8. View Slide

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

  10. View Slide

  11. View Slide

  12. why do we use swagger?

    View Slide

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

    View Slide

  14. View Slide

  15. go-swagger

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  19. and we're all set
    ?

    View Slide

  20. NO

    View Slide

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

  22. let’s fix ’em all!

    View Slide

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

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

    View Slide

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

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

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

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

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

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

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

  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

    View Slide

  37. bonus
    A pluggable go-swagger (in development)
    github.com/ilyakaznacheev/go-plugger

    View Slide

  38. bonus 2
    Insomnia Designer
    insomnia.rest/products/designer

    View Slide

  39. View Slide

  40. ilyakaznacheev

    View Slide