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

OpenAPI e Elixir (e qualquer outra linguagem)

OpenAPI e Elixir (e qualquer outra linguagem)

Lucas Mazza

August 03, 2024
Tweet

More Decks by Lucas Mazza

Other Decks in Programming

Transcript

  1. 3

  2. 4

  3. 5

  4. 6

  5. The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to

    HTTP APIs which allows both humans and computers to discover and understand the capabilities of the service [...] — https://www.openapis.org 8
  6. • 2011 - Swagger 1.0 • 2015 - OpenAPI 2.0

    • 2017 - OpenAPI 3.0 • 2021 - OpenAPI 3.1 9
  7. # cat doc/openapi.yml openapi: 3.1.0 info: title: Tremendous API #

    ... servers: # ... security: # ... paths: # ... components: # ... 11
  8. # cat doc/openapi.yml openapi: 3.1.0 info: title: Tremendous API #

    ... servers: - url: https://testflight.tremendous.com/api/v2 description: Sandbox environment - url: https://www.tremendous.com/api/v2 description: Production environment security: # ... paths: # ... components: # ... 12
  9. # cat doc/openapi.yml openapi: 3.1.0 info: # ... servers: #

    ... security: # ... paths: "/products": get: # ... post: # ... "/products/{id}": get: # ... components: # ... 13
  10. # cat doc/openapi.yml openapi: 3.1.0 info: # ... servers: #

    ... security: # ... paths: # ... components: schemas: # ... responses: # ... parameters: # ... examples: # ... requestBodies: # ... 14
  11. paths: "/posts": post: summary: Creates a new post on the

    blog operationId: createPost requestBody: content: application/json: schema: $ref: "#/components/schemas/PostParams" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Post" "422": content: application/json: schema: $ref: "#/components/schemas/ValidationErrors" 19
  12. paths: "/posts": post: summary: Creates a new post on the

    blog operationId: createPost requestBody: content: application/json: schema: $ref: "#/components/schemas/PostParams" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Post" "422": content: application/json: schema: $ref: "#/components/schemas/ValidationErrors" 19
  13. paths: "/posts": post: summary: Creates a new post on the

    blog operationId: createPost requestBody: content: application/json: schema: $ref: "#/components/schemas/PostParams" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Post" "422": content: application/json: schema: $ref: "#/components/schemas/ValidationErrors" 19
  14. paths: "/posts": post: summary: Creates a new post on the

    blog operationId: createPost requestBody: content: application/json: schema: $ref: "#/components/schemas/PostParams" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Post" "422": content: application/json: schema: $ref: "#/components/schemas/ValidationErrors" 19
  15. paths: "/posts": post: summary: Creates a new post on the

    blog operationId: createPost requestBody: content: application/json: schema: $ref: "#/components/schemas/PostParams" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Post" "422": content: application/json: schema: $ref: "#/components/schemas/ValidationErrors" 19
  16. paths: "/products": get: summary: Retrieve products operationId: listProducts parameters: -

    name: limit in: query required: false schema: type: integer default: 25 responses: "200": content: application/json: schema: $ref: "#/components/schemas/ProductsResponse" 21
  17. paths: "/products": get: summary: Retrieve products operationId: listProducts parameters: -

    name: limit in: query required: false schema: type: integer default: 25 responses: "200": content: application/json: schema: $ref: "#/components/schemas/ProductsResponse" 21
  18. paths: "/products": get: summary: Retrieve products operationId: listProducts parameters: -

    name: limit in: query required: false schema: type: integer default: 25 responses: "200": content: application/json: schema: $ref: "#/components/schemas/ProductsResponse" 21
  19. curl -H "Accept: application/json" \ https://myapp.dev/me # { # "id":

    "9e32ce59-8de9-42f6-b631-40381bd9aeb5", # "email": "[email protected]", # "last_signed_at": "2024-07-31T18:30:49.027203Z" # } 24
  20. curl -H "Accept: application/json" \ https://myapp.dev/me # { # "id":

    "9e32ce59-8de9-42f6-b631-40381bd9aeb5", # "email": "[email protected]", # "last_signed_at": "2024-07-31T18:30:49.027203Z" # } 24
  21. curl -H "Accept: application/json" \ https://myapp.dev/me # { # "id":

    "9e32ce59-8de9-42f6-b631-40381bd9aeb5", # "email": "[email protected]", # "last_signed_at": "2024-07-31T18:30:49.027203Z" # } 24
  22. components: schemas: CurrentUser: type: object properties: id: type: string format:

    uuid email: type: string format: email name: type: string last_signed_at: type: string format: date-time required: - id - email - last_signed_at 25
  23. components: schemas: CurrentUser: type: object properties: id: type: string format:

    uuid email: type: string format: email name: type: string last_signed_at: type: string format: date-time required: - id - email - last_signed_at 25
  24. components: schemas: CurrentUser: type: object properties: id: type: string format:

    uuid email: type: string format: email name: type: string last_signed_at: type: string format: date-time required: - id - email - last_signed_at 25
  25. paths: "/me": get: summary: Retrieves the current user operationId: getCurrentUser

    responses: "200": content: application/json: schema: $ref: "#/components/schemas/CurrentUser" 26
  26. components: schemas: TremendousId: type: string pattern: "[A-Z0-9]{12}" example: NQM23TPL3GH1 Product:

    type: object properties: id: $ref: "#/components/schemas/TremendousId" required: - id 27
  27. components: schemas: TremendousId: type: string pattern: "[A-Z0-9]{12}" example: NQM23TPL3GH1 Product:

    type: object properties: id: $ref: "#/components/schemas/TremendousId" required: - id 27
  28. paths: "/rewards/{id}": get: summary: Retrieves a single reward operationId: get-reward

    parameters: - name: id in: path required: true schema: $ref: "#/components/schemas/TremendousId" 28
  29. 29

  30. anyOf / allOf / oneOf components: schemas: BaseError: type: object

    # ... ValidationError: allOf: # `ValidationError` extende `BaseError` e include mais propriedades - $ref: "#/components/schemas/BaseError" - type: object properties: fields: type: array items: type: string 31
  31. anyOf / allOf / oneOf # `GET /users/{id}` response com

    um `User` ou um `BannedUser` # de acordo com o valor de `banned` paths: "/users/{id}": get: # ... responses: "200": content: application/json: schema: oneOf: - $ref: "#/components/schemas/User" - $ref: "#/components/schemas/BannedUser" # Opcional: discriminator: property_name: banned mapping: true: "#/components/schemas/BannedUser" false: "#/components/schemas/User" 32
  32. 33

  33. open_api_spex Leverage Open API Specification 3 (formerly Swagger) to document,

    test, validate and explore your Plug and Phoenix APIs. def deps do [ {:open_api_spex, "~> 3.18"} ] end 36
  34. defmodule MyAppWeb.ApiSpec do alias OpenApiSpex.{Info, OpenApi, Paths} alias MyAppWeb.Router @behaviour

    OpenApi @impl OpenApi def spec do %OpenApi{ servers: [], info: %Info{title: "My API", version: "1.0.0"}, paths: Paths.from_router(Router) } |> OpenApiSpex.resolve_schema_modules() end end 37
  35. defmodule MyAppWeb.ApiSpec do alias OpenApiSpex.{Info, OpenApi, Paths} alias MyAppWeb.Router @behaviour

    OpenApi @impl OpenApi def spec do %OpenApi{ servers: [], info: %Info{title: "My API", version: "1.0.0"}, paths: Paths.from_router(Router) } |> OpenApiSpex.resolve_schema_modules() end end 37
  36. defmodule MyAppWeb.ApiSpec do alias OpenApiSpex.{Info, OpenApi, Paths} alias MyAppWeb.Router @behaviour

    OpenApi @impl OpenApi def spec do %OpenApi{ servers: [], info: %Info{title: "My API", version: "1.0.0"}, paths: Paths.from_router(Router) } |> OpenApiSpex.resolve_schema_modules() end end 37
  37. defmodule MyAppWeb.ApiSpec do alias OpenApiSpex.{Info, OpenApi, Paths} alias MyAppWeb.Router @behaviour

    OpenApi @impl OpenApi def spec do %OpenApi{ servers: [], info: %Info{title: "My API", version: "1.0.0"}, paths: Paths.from_router(Router) } |> OpenApiSpex.resolve_schema_modules() end end 37
  38. # lib/my_app_web/router.ex pipeline :api do plug :accepts, ["json"] plug OpenApiSpex.Plug.PutApiSpec,

    module: MyAppWeb.ApiSpec end scope "/api" do pipe_through :api resources "/users", MyAppWeb.UserController, only: [:create, :index, :show] get "/openapi", OpenApiSpex.Plug.RenderSpec, [] end # mix openapi.spec.json --spec MyAppWeb.ApiSpec --filename doc/openapi.json # mix openapi.spec.yaml --spec MyAppWeb.ApiSpec --filename doc/openapi.yaml 38
  39. # lib/my_app_web/router.ex pipeline :api do plug :accepts, ["json"] plug OpenApiSpex.Plug.PutApiSpec,

    module: MyAppWeb.ApiSpec end scope "/api" do pipe_through :api resources "/users", MyAppWeb.UserController, only: [:create, :index, :show] get "/openapi", OpenApiSpex.Plug.RenderSpec, [] end # mix openapi.spec.json --spec MyAppWeb.ApiSpec --filename doc/openapi.json # mix openapi.spec.yaml --spec MyAppWeb.ApiSpec --filename doc/openapi.yaml 38
  40. # lib/my_app_web/router.ex pipeline :api do plug :accepts, ["json"] plug OpenApiSpex.Plug.PutApiSpec,

    module: MyAppWeb.ApiSpec end scope "/api" do pipe_through :api resources "/users", MyAppWeb.UserController, only: [:create, :index, :show] get "/openapi", OpenApiSpex.Plug.RenderSpec, [] end # mix openapi.spec.json --spec MyAppWeb.ApiSpec --filename doc/openapi.json # mix openapi.spec.yaml --spec MyAppWeb.ApiSpec --filename doc/openapi.yaml 38
  41. # lib/my_app_web/router.ex pipeline :api do plug :accepts, ["json"] plug OpenApiSpex.Plug.PutApiSpec,

    module: MyAppWeb.ApiSpec end scope "/api" do pipe_through :api resources "/users", MyAppWeb.UserController, only: [:create, :index, :show] get "/openapi", OpenApiSpex.Plug.RenderSpec, [] end # mix openapi.spec.json --spec MyAppWeb.ApiSpec --filename doc/openapi.json # mix openapi.spec.yaml --spec MyAppWeb.ApiSpec --filename doc/openapi.yaml 38
  42. defmodule MyAppWeb.UserController do use MyAppWeb, :controller use OpenApiSpex.ControllerSpecs alias MyAppWeb.Schemas.{UserParams,

    UserResponse} operation :update, summary: "Update user", parameters: [ id: [in: :path, description: "User ID", type: :integer, example: 1001] ], request_body: {"User params", "application/json", UserParams}, responses: [ ok: {"User response", "application/json", UserResponse} ] def update(conn, %{"id" => id}) do # ... end end 40
  43. defmodule MyAppWeb.UserController do use MyAppWeb, :controller use OpenApiSpex.ControllerSpecs alias MyAppWeb.Schemas.{UserParams,

    UserResponse} operation :update, summary: "Update user", parameters: [ id: [in: :path, description: "User ID", type: :integer, example: 1001] ], request_body: {"User params", "application/json", UserParams}, responses: [ ok: {"User response", "application/json", UserResponse} ] def update(conn, %{"id" => id}) do # ... end end 40
  44. defmodule MyAppWeb.UserController do use MyAppWeb, :controller use OpenApiSpex.ControllerSpecs alias MyAppWeb.Schemas.{UserParams,

    UserResponse} operation :update, summary: "Update user", parameters: [ id: [in: :path, description: "User ID", type: :integer, example: 1001] ], request_body: {"User params", "application/json", UserParams}, responses: [ ok: {"User response", "application/json", UserResponse} ] def update(conn, %{"id" => id}) do # ... end end 40
  45. paths: /api/users/{id}: put: operationId: MyAppWeb.UserController.update parameters: - description: User ID

    example: 1001 in: path name: id required: true schema: type: integer requestBody: content: application/json: schema: $ref: "#/components/schemas/UserParams" description: User params required: false responses: 200: content: application/json: schema: $ref: "#/components/schemas/UserResponse" description: User response summary: Update user patch: # ... 43
  46. defmodule MyAppWeb.Schemas.User do alias OpenApiSpex.Schema require OpenApiSpex OpenApiSpex.schema(%{ type: :object,

    properties: %{ id: %Schema{type: :integer, description: "User ID"}, name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/}, email: %Schema{type: :string, description: "Email address", format: :email}, birthday: %Schema{type: :string, description: "Birth date", format: :date}, inserted_at: %Schema{ type: :string, description: "Creation timestamp", format: :"date-time" }, updated_at: %Schema{type: :string, description: "Update timestamp", format: :"date-time"} }, required: [:name, :email], }) end 45
  47. defmodule MyAppWeb.Schemas.User do alias OpenApiSpex.Schema @behaviour OpenApiSpex.Schema def schema do

    %Schema{ title: "User", type: :object, properties: %{ id: %Schema{type: :integer, description: "User ID"}, name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/}, email: %Schema{type: :string, description: "Email address", format: :email}, birthday: %Schema{type: :string, description: "Birth date", format: :date}, inserted_at: %Schema{ type: :string, description: "Creation timestamp", format: :"date-time" }, updated_at: %Schema{type: :string, description: "Update timestamp", format: :"date-time"} }, required: [:name, :email], } end end 46
  48. fixtures & assertions defmodule MyAppWeb.Schemas.UserParams do alias OpenApiSpex.Schema @behaviour OpenApiSpex.Schema

    def schema do %Schema{ title: "UserParams", type: :object, properties: %{ # ... }, required: [:user], example: %{ user: %{ name: "John Wick", email: "[email protected]", birthday: "1964-07-12" } } } end end 50
  49. fixtures & assertions defmodule MyAppWeb.Schemas.UserParams do alias OpenApiSpex.Schema @behaviour OpenApiSpex.Schema

    def schema do %Schema{ title: "UserParams", type: :object, properties: %{ # ... }, required: [:user], example: %{ user: %{ name: "John Wick", email: "[email protected]", birthday: "1964-07-12" } } } end end 50
  50. fixtures & assertions import OpenApiSpex.TestAssertions test "renders user when data

    is valid", %{conn: conn, user: %User{id: id} = user} do api_spec = MyAppWeb.ApiSpec.spec() params = OpenApiSpex.Schema.example(MyAppWeb.Schemas.UserParams.schema()) assert_schema(params, "UserParams", api_spec) conn |> put(~p"/api/users/#{id}", params) |> json_response(200) |> assert_schema("UserResponse", api_spec) end 51
  51. fixtures & assertions import OpenApiSpex.TestAssertions test "renders user when data

    is valid", %{conn: conn, user: %User{id: id} = user} do api_spec = MyAppWeb.ApiSpec.spec() params = OpenApiSpex.Schema.example(MyAppWeb.Schemas.UserParams.schema()) assert_schema(params, "UserParams", api_spec) conn |> put(~p"/api/users/#{id}", params) |> json_response(200) |> assert_schema("UserResponse", api_spec) end 51
  52. fixtures & assertions 1) test update user renders user when

    data is valid (MyAppWeb.UserControllerTest) test/my_app_web/controllers/user_controller_test.exs:53 Value does not conform to schema UserResponse: Missing field: name at /user/name Missing field: email at /user/email %{"user" => %{"birthday" => "1964-07-12", "id" => 15, "inserted_at" => "2024-07-27T23:14:09Z", "updated_at" => "2024-07-27T23:14:09Z"}} code: |> assert_schema("UserResponse", api_spec) stacktrace: test/my_app_web/controllers/user_controller_test.exs:62: (test) 52
  53. fixtures & assertions ! " # ! assert company =

    json["data"]["company"] assert user = json["data"]["user"] assert user["name"] assert user["email"] assert user["profile_picture"] assert company["vat"] assert company["billing_email"] assert company["name"] assert company["slug"] assert Map.has_key?(company, "msa_effective_date") assert Map.has_key?(company, "registration_number") ! " # ! 53
  54. fixtures & assertions components: schemas: UserParams: example: user: birthday: 1964-07-12

    email: [email protected] name: John Wick properties: # ... title: UserParams type: object 54
  55. fixtures & assertions components: schemas: UserParams: example: user: birthday: 1964-07-12

    email: [email protected] name: John Wick properties: # ... title: UserParams type: object 54
  56. fixtures & assertions defmodule MyAppWeb.Schemas.ExamplesTest do use ExUnit.Case, async: true

    import OpenApiSpex.TestAssertions test "all examples are valid" do api_spec = MyAppWeb.ApiSpec.spec() for {name, schema} <- api_spec.components.schemas do example = OpenApiSpex.Schema.example(schema) assert_schema(example, name, api_spec) end end end 55
  57. enums defmodule MyApp.Users.User do use Ecto.Schema import Ecto.Changeset schema "users"

    do # ... field :status, Ecto.Enum, values: [:invited, :active, :deleted] end end 57
  58. enums defmodule MyApp.Users.User do use Ecto.Schema import Ecto.Changeset schema "users"

    do # ... field :status, Ecto.Enum, values: [:invited, :active, :deleted] end end 57
  59. enums defmodule MyAppWeb.Schemas.Enum do alias OpenApiSpex.Schema def to_schema(module, field, opts

    \\ []) do values = Ecto.Enum.values(module, field) example = Keyword.get(opts, :example, Enum.fetch!(values, 0)) suffix = module |> Module.split() |> List.last() title = Keyword.get(opts, :title, "#{suffix}#{String.capitalize(to_string(field))}") %Schema{ title: title, type: type, enum: values, example: example, } end end 58
  60. enums defmodule MyAppWeb.Schemas.User do alias OpenApiSpex.Schema @behaviour OpenApiSpex.Schema def schema

    do %Schema{ title: "User", type: :object, properties: %{ # ... status: MyAppWeb.Schemas.UserStatus, } } end end 60
  61. enums components: schemas: User: properties: # ... status: $ref: '#/components/schemas/UserStatus'

    # ... UserStatus: enum: - invited - active - deleted example: invited title: User status type: string 61
  62. plugs # Expõe o Swagger UI junto da sua spec

    OpenApiSpex.Plug.SwaggerUI # Atualiza `conn.params` e `conn.body_params` de acordo com a spec OpenApiSpex.Plug.Cast # Responde com `422` caso a requisição seja inválida OpenApiSpex.Plug.Validate OpenApiSpex.Plug.CastAndValidate 62
  63. open_api_spex • ✅ projeto estável e ativo • ✅ sem

    dependências • ✅ schema em Elixir é muito extensível 63
  64. open_api_spex • ✅ projeto estável e ativo • ✅ sem

    dependências • ✅ schema em Elixir é muito extensível • " macros 63
  65. open_api_spex • ✅ projeto estável e ativo • ✅ sem

    dependências • ✅ schema em Elixir é muito extensível • " macros • " pode gerar spec inválida 63
  66. open_api_spex • ✅ projeto estável e ativo • ✅ sem

    dependências • ✅ schema em Elixir é muito extensível • " macros • " pode gerar spec inválida • " Não tem suporte completo a spec 3.1 63
  67. 68

  68. 69

  69. 70

  70. mocks https://github.com/stoplightio/prism npx @stoplight/prism-cli mock ../backend-app/openapi.yml # Prism is listening

    on http://127.0.0.1:4010 npx @stoplight/prism-cli proxy ../backend-app/openapi.yml https://myapp.dev/ 74
  71. wrappers / sdks • https://buildwithfern.com/ - Saas / OSS •

    https://www.speakeasy.com/ - Saas • https://openapi-generator.tech/ - OSS 76
  72. openapi-generator brew install openapi-generator # openjdk, lol # https://remote.com/resources/api/reference openapi-generator

    generate \ --input-spec remote.json \ --generator-name ruby \ --additional-properties gemName=remote \ --output ./remote-ruby-sdk 77
  73. openapi-generator # docker docker run --rm \ -v ./remote-ruby-sdk:/remote-ruby-sdk \

    -v ./remote.json:/remote.json \ openapitools/openapi-generator-cli generate \ --input-spec /remote.json \ --generator-name ruby \ --additional-properties gemName=remote \ --output /remote-ruby-sdk 78
  74. openapi-generator # cd ./remote-ruby-sdk # bundle install # irb -Ilib

    require "remote" Remote.configure do |config| config.access_token = ENV["ACCESS_TOKEN"] end countries = Remote::CountriesApi.new response = countries.get_index_holiday("", "BRA", "2024") response.data.last(5).each do |holiday| puts "* #{holiday.observed_day} - #{holiday.name}" end # * 2024-10-12 - Our Lady of Aparecida (Nossa Senhora Aparecida) # * 2024-11-02 - Day of the Dead (Dia de Finados) # * 2024-11-15 - Proclamation of the Republic (Proclamação da República) # * 2024-11-20 - Black Awareness Day (Dia da Consciência Negra) # * 2024-12-25 - Christmas Day (Natal) 79
  75. openapi-generator openapi-generator generate \ --input-spec remote.json \ --generator-name elixir \

    --additional-properties packageName=remote \ --output ./remote-elixir-sdk 80
  76. # cd ./remote-elixir-sdk # mix deps.get # iex -S mix

    alias RemoteAPI.Api.Countries alias RemoteAPI.Connection token = System.get_env("ACCESS_TOKEN") conn = Connection.new with {:ok, %{data: data}} <- Countries.get_index_holiday(conn, "Bearer #{token}", "BRA", "2024"), holidays <- Enum.take(data, -5) do for holiday <- holidays do IO.puts("* #{holiday.observed_day} - #{holiday.name}") end end # * 2024-10-12 - Our Lady of Aparecida (Nossa Senhora Aparecida) # * 2024-11-02 - Day of the Dead (Dia de Finados) # * 2024-11-15 - Proclamation of the Republic (Proclamação da República) # * 2024-11-20 - Black Awareness Day (Dia da Consciência Negra) # * 2024-12-25 - Christmas Day (Natal) 81
  77. 82

  78. openapi-generator (e todos os outros) • entidades & documentação •

    templates/extensões • ~80% código gerado, ~20% manual 83
  79. openapi-generator (e todos os outros) • entidades & documentação •

    templates/extensões • ~80% código gerado, ~20% manual • (testes, exemplos, etc) 83
  80. tremendous-node import { Configuration, CreateOrderRequest, Environments, OrdersApi, } from "tremendous";

    const configuration = new Configuration({ basePath: Environments.production, accessToken: "YOUR-PRODUCTION-TOKEN", }); const orders = new OrdersApi(configuration); const params: CreateOrderRequest = { /* ... */ }; const { data } = await orders.createOrder(params); console.log(`Order created! ID: ${data.order.id}`); 85
  81. tremendous-python from tremendous import Configuration, ApiClient, TremendousApi configuration = Configuration(

    server_index=Configuration.Environment["production"], access_token="YOUR-PRODUCTION-TOKEN" ) client = TremendousApi(ApiClient(configuration)) params = CreateOrderRequest( # ... ) response = client.create_order(request) print("Order created! ID: %s" % response.order.id) 86
  82. recap • OpenAPI <3 HTTP/REST • yaml/json/#{sua linguagem aqui} •

    útil para documentação, testes e dev • 80% tooling, 20% você 87
  83. recap • OpenAPI <3 HTTP/REST • yaml/json/#{sua linguagem aqui} •

    útil para documentação, testes e dev • 80% tooling, 20% você • melhor do que fazer na mão 87