From abe90e3db0cab2e75ede364ee24f26c9e490f74f Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Mon, 3 Mar 2025 19:30:45 +0200 Subject: [PATCH 01/23] Bump CI dependencies (#655) --- .github/workflows/elixir.yml | 14 +++++++------- mix.lock | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 1a84de79..19c61bdc 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -15,14 +15,14 @@ jobs: otp: ["26"] elixir: ["1.16"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 id: beam with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - name: PLT cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: key: | ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt @@ -34,7 +34,7 @@ jobs: - run: mix compile --warnings-as-errors - run: mix format --check-formatted - run: mix credo --strict --all - - run: mix dialyzer + - run: mix dialyzer --format github test_examples: runs-on: ubuntu-latest @@ -42,12 +42,12 @@ jobs: env: MIX_ENV: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: otp-version: 26 elixir-version: 1.16 - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: key: | ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plug-build @@ -77,12 +77,12 @@ jobs: - { otp: "23", elixir: "1.16" } - { otp: "25", elixir: "1.16" } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: key: | ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-build diff --git a/mix.lock b/mix.lock index 605a5da4..53da2382 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,18 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, - "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "phoenix": {:hex, :phoenix, "1.6.6", "281c8ce8dccc9f60607346b72cdfc597c3dde134dd9df28dff08282f0b751754", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "807bd646e64cd9dc83db016199715faba72758e6db1de0707eef0a2da4924364"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, From 4a3f7a39c195af8474a0a9fb0a62377abb61099b Mon Sep 17 00:00:00 2001 From: Georges Dubus Date: Fri, 7 Mar 2025 16:08:15 +0100 Subject: [PATCH 02/23] Add examples property to Schema (#654) Fixes #653 --- lib/open_api_spex/schema.ex | 6 ++++++ test/schema_test.exs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 44d70f9b..5c585e13 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -183,6 +183,7 @@ defmodule OpenApiSpex.Schema do :xml, :externalDocs, :example, + :examples, :deprecated, :"x-struct", :"x-validate", @@ -250,6 +251,7 @@ defmodule OpenApiSpex.Schema do xml: Xml.t() | nil, externalDocs: ExternalDocumentation.t() | nil, example: any, + examples: [any] | nil, deprecated: boolean | nil, "x-struct": module | nil, "x-validate": module | nil, @@ -369,6 +371,10 @@ defmodule OpenApiSpex.Schema do schema.example end + def example(%Schema{examples: [example | _]}) when not is_nil(example) do + example + end + def example(%Schema{enum: [example | _]}) do example end diff --git a/test/schema_test.exs b/test/schema_test.exs index e44615d5..43b70154 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -103,6 +103,10 @@ defmodule OpenApiSpex.SchemaTest do assert Schema.example(%Schema{type: :string, example: "foo"}) == "foo" end + test "uses the first value in `examples` property when not nil" do + assert Schema.example(%Schema{type: :string, examples: ["foo", "bar"]}) == "foo" + end + test "defaults to type-appropriate value for :string" do assert Schema.example(%Schema{type: :string}) == "" end From 439fadc5bda8e316bc4b83eecb48b1f9ecc2a5bb Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Fri, 7 Mar 2025 17:50:48 +0200 Subject: [PATCH 03/23] Document schema resolver duplicate titles behaviour (#656) --- lib/open_api_spex.ex | 12 +- lib/open_api_spex/schema_resolver.ex | 20 +- test/schema_resolver_test.exs | 442 +++++++++++++++------------ 3 files changed, 281 insertions(+), 193 deletions(-) diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex index a7d18c74..9db3db9d 100644 --- a/lib/open_api_spex.ex +++ b/lib/open_api_spex.ex @@ -29,7 +29,17 @@ defmodule OpenApiSpex do Then the `UserResponse.schema()` function will be called to load the schema, and a `Reference` to the loaded schema will be used in the operation response. - See `OpenApiSpex.schema` macro for a convenient syntax for defining schema modules. + See `OpenApiSpex.schema/2` macro for a convenient syntax for defining schema modules. + + > #### Known Issues {: .info} + > + > Resolving schemas expects the schema title to be unique for the generated references to be unique. + > + > For schemas defined with the `OpenApiSpex.schema/2` macro, the title is automatically set + > to the last part of module name. For example `MyAppWeb.Schemas.User` will have the title `"User"`, + > and `MyAppWeb.OtherSchemas.User` **will also** have the title `"User"` which can lead to conflicts. + > + > The recommendation is to set the title explicitly in the schema definition. """ @spec resolve_schema_modules(OpenApi.t()) :: OpenApi.t() def resolve_schema_modules(spec = %OpenApi{}) do diff --git a/lib/open_api_spex/schema_resolver.ex b/lib/open_api_spex/schema_resolver.ex index ea2bd0b2..24e8b06d 100644 --- a/lib/open_api_spex/schema_resolver.ex +++ b/lib/open_api_spex/schema_resolver.ex @@ -2,6 +2,7 @@ defmodule OpenApiSpex.SchemaResolver do @moduledoc """ Internal module used to resolve `OpenApiSpex.Schema` structs from atoms. """ + alias OpenApiSpex.Discriminator alias OpenApiSpex.{ @@ -29,7 +30,17 @@ defmodule OpenApiSpex.SchemaResolver do Then the `UserResponse.schema()` function will be called to load the schema, and a `Reference` to the loaded schema will be used in the operation response. - See `OpenApiSpex.schema` macro for a convenient syntax for defining schema modules. + See `OpenApiSpex.schema/2` macro for a convenient syntax for defining schema modules. + + > #### Known Issues {: .info} + > + > Resolving schemas expects the schema title to be unique for the generated references to be unique. + > + > For schemas defined with the `OpenApiSpex.schema/2` macro, the title is automatically set + > to the last part of module name. For example `MyAppWeb.Schemas.User` will have the title `"User"`, + > and `MyAppWeb.OtherSchemas.User` **will also** have the title `"User"` which can lead to conflicts. + > + > The recommendation is to set the title explicitly in the schema definition. """ @spec resolve_schema_modules(OpenApi.t()) :: OpenApi.t() def resolve_schema_modules(spec = %OpenApi{}) do @@ -197,14 +208,15 @@ defmodule OpenApiSpex.SchemaResolver do Enum.map_reduce(schema_list, schemas, &resolve_schema_modules_from_schema/2) end - defp resolve_schema_modules_from_schema(schema, schemas) when is_atom(schema) do - title = schema.schema().title + defp resolve_schema_modules_from_schema(schema_module, schemas) when is_atom(schema_module) do + schema = schema_module.schema() + title = schema.title new_schemas = if Map.has_key?(schemas, title) do schemas else - {new_schema, schemas} = resolve_schema_modules_from_schema(schema.schema(), schemas) + {new_schema, schemas} = resolve_schema_modules_from_schema(schema, schemas) Map.put(schemas, title, new_schema) end diff --git a/test/schema_resolver_test.exs b/test/schema_resolver_test.exs index 23f13b9c..18fc8aeb 100644 --- a/test/schema_resolver_test.exs +++ b/test/schema_resolver_test.exs @@ -14,206 +14,208 @@ defmodule OpenApiSpex.SchemaResolverTest do SchemaResolver } - test "Resolves schemas in OpenApi spec" do - spec = %OpenApi{ - info: %Info{ - title: "Test", - version: "1.0.0" - }, - paths: %{ - "/api/users" => %PathItem{ - get: %Operation{ - responses: %{ - 200 => %Response{ - description: "Success", - content: %{ - "application/json" => %MediaType{ - schema: OpenApiSpexTest.Schemas.UsersResponse + describe "resolve_schema_modules/1" do + test "resolves schemas in OpenApi spec" do + spec = %OpenApi{ + info: %Info{ + title: "Test", + version: "1.0.0" + }, + paths: %{ + "/api/users" => %PathItem{ + get: %Operation{ + responses: %{ + 200 => %Response{ + description: "Success", + content: %{ + "application/json" => %MediaType{ + schema: OpenApiSpexTest.Schemas.UsersResponse + } } } } - } - }, - post: %Operation{ - description: "Create a user", - operationId: "UserController.create", - requestBody: %RequestBody{ - content: %{ - "application/json" => %MediaType{ - schema: OpenApiSpexTest.Schemas.UserRequest - } - } }, - responses: %{ - 201 => %Response{ - description: "Created", + post: %Operation{ + description: "Create a user", + operationId: "UserController.create", + requestBody: %RequestBody{ content: %{ "application/json" => %MediaType{ - schema: OpenApiSpexTest.Schemas.UserResponse + schema: OpenApiSpexTest.Schemas.UserRequest + } + } + }, + responses: %{ + 201 => %Response{ + description: "Created", + content: %{ + "application/json" => %MediaType{ + schema: OpenApiSpexTest.Schemas.UserResponse + } } } } } - } - }, - "/api/users/{id}/payment_details" => %PathItem{ - get: %Operation{ - responses: %{ - 200 => %Response{ - description: "Success", - content: %{ - "application/json" => %MediaType{ - schema: OpenApiSpexTest.Schemas.PaymentDetails + }, + "/api/users/{id}/payment_details" => %PathItem{ + get: %Operation{ + responses: %{ + 200 => %Response{ + description: "Success", + content: %{ + "application/json" => %MediaType{ + schema: OpenApiSpexTest.Schemas.PaymentDetails + } } } } } - } - }, - "/api/users/{id}/friends" => %PathItem{ - get: %Operation{ - parameters: [ - %OpenApiSpex.Parameter{ - name: :id, - in: :path, - schema: %Schema{type: :integer} - } - ], - responses: %{ - 200 => %Response{ - description: "Success", - content: %{ - "application/json" => %MediaType{ - schema: %Schema{ - type: :array, - items: OpenApiSpexTest.Schemas.User + }, + "/api/users/{id}/friends" => %PathItem{ + get: %Operation{ + parameters: [ + %OpenApiSpex.Parameter{ + name: :id, + in: :path, + schema: %Schema{type: :integer} + } + ], + responses: %{ + 200 => %Response{ + description: "Success", + content: %{ + "application/json" => %MediaType{ + schema: %Schema{ + type: :array, + items: OpenApiSpexTest.Schemas.User + } } } } } } - } - }, - "/api/users/subscribe" => %PathItem{ - post: %Operation{ - description: "Subscribe to user updates", - operationId: "UserController.subscribe", - requestBody: %RequestBody{ - required: true, - content: %{ - "application/json" => %MediaType{ - schema: OpenApiSpexTest.Schemas.UserSubscribeRequest + }, + "/api/users/subscribe" => %PathItem{ + post: %Operation{ + description: "Subscribe to user updates", + operationId: "UserController.subscribe", + requestBody: %RequestBody{ + required: true, + content: %{ + "application/json" => %MediaType{ + schema: OpenApiSpexTest.Schemas.UserSubscribeRequest + } } - } - }, - callbacks: %{ - "user_updated" => %{ - "{$request.body#/callback_url}" => %PathItem{ - post: %Operation{ - description: "Provided endpoint for sending updates", - requestBody: %RequestBody{ - required: true, - content: %{ - "application/json" => %MediaType{ - schema: OpenApiSpexTest.Schemas.UserResponse + }, + callbacks: %{ + "user_updated" => %{ + "{$request.body#/callback_url}" => %PathItem{ + post: %Operation{ + description: "Provided endpoint for sending updates", + requestBody: %RequestBody{ + required: true, + content: %{ + "application/json" => %MediaType{ + schema: OpenApiSpexTest.Schemas.UserResponse + } + } + }, + responses: %{ + 200 => %Response{ + description: "Your server returns this code if it accepts the callback" } - } - }, - responses: %{ - 200 => %Response{ - description: "Your server returns this code if it accepts the callback" } } } } - } - }, - responses: %{ - 201 => %Response{ - description: "Webhook created" + }, + responses: %{ + 201 => %Response{ + description: "Webhook created" + } } } - } - }, - "/api/appointsments" => %PathItem{ - post: %Operation{ - description: "Create a new pet appointment", - operationId: "PetAppointmentController.create", - requestBody: %RequestBody{ - required: true, - content: %{ - "application/json" => %MediaType{ - schema: OpenApiSpexTest.Schemas.PetAppointmentRequest + }, + "/api/appointsments" => %PathItem{ + post: %Operation{ + description: "Create a new pet appointment", + operationId: "PetAppointmentController.create", + requestBody: %RequestBody{ + required: true, + content: %{ + "application/json" => %MediaType{ + schema: OpenApiSpexTest.Schemas.PetAppointmentRequest + } + } + }, + responses: %{ + 201 => %Response{ + description: "Appointment created" } - } - }, - responses: %{ - 201 => %Response{ - description: "Appointment created" } } } } } - } - resolved = OpenApiSpex.resolve_schema_modules(spec) + resolved = OpenApiSpex.resolve_schema_modules(spec) - assert %Reference{"$ref": "#/components/schemas/UsersResponse"} = - resolved.paths["/api/users"].get.responses[200].content["application/json"].schema + assert %Reference{"$ref": "#/components/schemas/UsersResponse"} = + resolved.paths["/api/users"].get.responses[200].content["application/json"].schema - assert %Reference{"$ref": "#/components/schemas/UserResponse"} = - resolved.paths["/api/users"].post.responses[201].content["application/json"].schema + assert %Reference{"$ref": "#/components/schemas/UserResponse"} = + resolved.paths["/api/users"].post.responses[201].content["application/json"].schema - assert %Reference{"$ref": "#/components/schemas/UserRequest"} = - resolved.paths["/api/users"].post.requestBody.content["application/json"].schema + assert %Reference{"$ref": "#/components/schemas/UserRequest"} = + resolved.paths["/api/users"].post.requestBody.content["application/json"].schema - assert "#/components/schemas/TrainingAppointment" = - resolved.components.schemas["PetAppointmentRequest"].discriminator.mapping[ - "training" - ] + assert "#/components/schemas/TrainingAppointment" = + resolved.components.schemas["PetAppointmentRequest"].discriminator.mapping[ + "training" + ] - assert "#/components/schemas/GroomingAppointment" = - resolved.components.schemas["PetAppointmentRequest"].discriminator.mapping[ - "grooming" - ] + assert "#/components/schemas/GroomingAppointment" = + resolved.components.schemas["PetAppointmentRequest"].discriminator.mapping[ + "grooming" + ] - assert %{ - "UserRequest" => %Schema{}, - "UserResponse" => %Schema{}, - "User" => %Schema{}, - "UserSubscribeRequest" => %Schema{}, - "PaymentDetails" => %Schema{}, - "CreditCardPaymentDetails" => %Schema{}, - "DirectDebitPaymentDetails" => %Schema{}, - "PetAppointmentRequest" => %Schema{}, - "TrainingAppointment" => %Schema{}, - "GroomingAppointment" => %Schema{} - } = resolved.components.schemas + assert %{ + "UserRequest" => %Schema{}, + "UserResponse" => %Schema{}, + "User" => %Schema{}, + "UserSubscribeRequest" => %Schema{}, + "PaymentDetails" => %Schema{}, + "CreditCardPaymentDetails" => %Schema{}, + "DirectDebitPaymentDetails" => %Schema{}, + "PetAppointmentRequest" => %Schema{}, + "TrainingAppointment" => %Schema{}, + "GroomingAppointment" => %Schema{} + } = resolved.components.schemas - get_friends = resolved.paths["/api/users/{id}/friends"].get + get_friends = resolved.paths["/api/users/{id}/friends"].get - assert %Reference{"$ref": "#/components/schemas/User"} = - get_friends.responses[200].content["application/json"].schema.items - end + assert %Reference{"$ref": "#/components/schemas/User"} = + get_friends.responses[200].content["application/json"].schema.items + end - test "raises informative error when schema :properties is not a map" do - spec = %OpenApi{ - info: %Info{ - title: "Test", - version: "1.0.0" - }, - paths: %{ - "/api/users" => %PathItem{ - get: %Operation{ - responses: %{ - 200 => %Response{ - description: "Success", - content: %{ - "application/json" => %MediaType{ - schema: %Schema{ - type: :object, - properties: Invalid + test "raises informative error when schema :properties is not a map" do + spec = %OpenApi{ + info: %Info{ + title: "Test", + version: "1.0.0" + }, + paths: %{ + "/api/users" => %PathItem{ + get: %Operation{ + responses: %{ + 200 => %Response{ + description: "Success", + content: %{ + "application/json" => %MediaType{ + schema: %Schema{ + type: :object, + properties: Invalid + } } } } @@ -222,28 +224,28 @@ defmodule OpenApiSpex.SchemaResolverTest do } } } - } - assert_raise RuntimeError, "Expected :properties to be a map. Got: Invalid", fn -> - OpenApiSpex.resolve_schema_modules(spec) + assert_raise RuntimeError, "Expected :properties to be a map. Got: Invalid", fn -> + OpenApiSpex.resolve_schema_modules(spec) + end end - end - test "raises error when schema cannot be resolved" do - spec = %OpenApi{ - info: %Info{ - title: "Test", - version: "1.0.0" - }, - paths: %{ - "/api/users" => %PathItem{ - get: %Operation{ - responses: %{ - 200 => %Response{ - description: "Success", - content: %{ - "application/json" => %MediaType{ - schema: %{} + test "raises error when schema cannot be resolved" do + spec = %OpenApi{ + info: %Info{ + title: "Test", + version: "1.0.0" + }, + paths: %{ + "/api/users" => %PathItem{ + get: %Operation{ + responses: %{ + 200 => %Response{ + description: "Success", + content: %{ + "application/json" => %MediaType{ + schema: %{} + } } } } @@ -251,22 +253,86 @@ defmodule OpenApiSpex.SchemaResolverTest do } } } - } - error_message = """ - Cannot resolve schema %{}. + error_message = """ + Cannot resolve schema %{}. - Must be one of: + Must be one of: - - schema module, or schema struct - - list of schema modules, or schema structs - - boolean - - nil - - reference - """ + - schema module, or schema struct + - list of schema modules, or schema structs + - boolean + - nil + - reference + """ + + assert_raise RuntimeError, error_message, fn -> + OpenApiSpex.resolve_schema_modules(spec) + end + end + + defmodule Duplicates.UsersResponse do + require OpenApiSpex + + OpenApiSpex.schema(%{ + description: "A duplicate", + type: :object, + properties: %{ + unexpected: %Schema{type: :string} + } + }) + end + + test "resolution ignores schemas with duplicate title" do + spec = %OpenApi{ + info: %Info{ + title: "Test", + version: "1.0.0" + }, + paths: %{ + "/api/users" => %PathItem{ + get: %Operation{ + responses: %{ + 200 => %Response{ + description: "Success", + content: %{ + "application/json" => %MediaType{ + schema: OpenApiSpexTest.Schemas.UsersResponse + } + } + } + } + } + }, + "/api/clones" => %PathItem{ + get: %Operation{ + responses: %{ + 200 => %Response{ + description: "Success", + content: %{ + "application/json" => %MediaType{ + schema: Duplicates.UsersResponse + } + } + } + } + } + } + } + } + + assert %{ + components: %{ + schemas: %{ + "UsersResponse" => %OpenApiSpex.Schema{ + description: "A duplicate", + "x-struct": OpenApiSpex.SchemaResolverTest.Duplicates.UsersResponse + } + } + } + } = resolved = OpenApiSpex.resolve_schema_modules(spec) - assert_raise RuntimeError, error_message, fn -> - OpenApiSpex.resolve_schema_modules(spec) + assert Map.keys(resolved.components.schemas) == ["UsersResponse"] end end From 02d8c1558a4e0071b44403dd74b477fc9648174a Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Fri, 7 Mar 2025 18:53:42 +0200 Subject: [PATCH 04/23] Add spec.yaml tasks to example applications (#657) * Add mix spec.yaml tasks in example applications * Update example application dependencies --- examples/phoenix_app/mix.exs | 4 +++- examples/phoenix_app/mix.lock | 35 ++++++++++++++++++----------------- examples/plug_app/mix.exs | 9 +++++---- examples/plug_app/mix.lock | 23 ++++++++++++----------- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/examples/phoenix_app/mix.exs b/examples/phoenix_app/mix.exs index 7164e7b4..8e4111ac 100644 --- a/examples/phoenix_app/mix.exs +++ b/examples/phoenix_app/mix.exs @@ -31,7 +31,8 @@ defmodule PhoenixApp.Mixfile do defp aliases() do [ test: ["ecto.create --quiet", "ecto.migrate", "test"], - spec: ["openapi.spec.json --spec PhoenixAppWeb.ApiSpec \"priv/static/swagger.json\""] + spec: ["openapi.spec.json --spec PhoenixAppWeb.ApiSpec \"priv/static/swagger.json\""], + "spec.yaml": ["openapi.spec.yaml --spec PhoenixAppWeb.ApiSpec \"priv/static/swagger.yaml\""] ] end @@ -48,6 +49,7 @@ defmodule PhoenixApp.Mixfile do {:phoenix_view, "~> 2.0"}, {:plug_cowboy, "~> 2.0"}, {:jason, "~> 1.0"}, + {:ymlr, "~> 5.0"}, {:dialyxir, "1.0.0-rc.6", only: [:dev], runtime: false} ] end diff --git a/examples/phoenix_app/mix.lock b/examples/phoenix_app/mix.lock index b8974b90..320d3a8a 100644 --- a/examples/phoenix_app/mix.lock +++ b/examples/phoenix_app/mix.lock @@ -1,45 +1,46 @@ %{ - "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, + "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, - "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "49496d63267bc1a4614ffd5f67c45d9fc3ea62701a6797975bc98bc156d2763f"}, - "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"}, - "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.18.1", "63c4b01cbb67f5e7eecde6819bc050fbb835141b534b779f80f40d479cdce669", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "ce223f0eda408437e7dc2b5f2a9dab306c7a709dc865f96172ba5eadfea3ce48"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "esqlite": {:hex, :esqlite, "0.8.8", "3b64d09ce56e46607040861e9febe921aa3c471b143fa90be86078ef899d70c8", [:rebar3], [], "hexpm", "374902457c7d94dc9409c98d3bdd1ca0d50a60dc9f3bdf1fd8eb74c0dcdf02d6"}, - "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, + "exqlite": {:hex, :exqlite, "0.29.0", "e6f1de4bfe3ce6e4c4260b15fef830705fa36632218dc7eafa0a5aba3a5d6e04", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a75f8a069fcdad3e5f95dfaddccd13c2112ea3b742fdcc234b96410e9c1bde00"}, + "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, + "httpoison": {:hex, :httpoison, "2.2.2", "15420e9e5bbb505b931b2f589dc8be0c3b21e2a91a2c6ba882d99bf8f3ad499d", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "de7ac49fe2ffd89219972fdf39b268582f6f7f68d8cd29b4482dacca1ce82324"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix": {:hex, :phoenix, "1.7.20", "6bababaf27d59f5628f9b608de902a021be2cecefb8231e1dbdc0a2e2e480e9b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6be2ab98302e8784a31829e0d50d8bdfa81a23cd912c395bafd8b8bfb5a086c2"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm", "ba952bfa35b374e1e5d84bc5f5efe8360c6f99dc93b3118f714a9a2dff6c9e19"}, "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.4.1", "a292b807c7670f1c2c136f7224934bf5929c998ea1b9ded63b710f3dd5140e4c", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.6", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm", "19358813676eb058d1b0630cd6914817e7dbf001022c49159510241f14f0eea7"}, "sqlitex": {:hex, :sqlitex, "1.7.1", "022d477aab2ae999c43ae6fbd1782ff1457e0e95c251c7b5fa6f7b7b102040ff", [:mix], [{:decimal, "~> 1.7", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm", "ef16cda37b151136a47a6c0830dc9eb5e5f8f5f029b649e9f3a58a6eed634b80"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "ymlr": {:hex, :ymlr, "5.1.3", "a8061add5a378e20272a31905be70209a5680fdbe0ad51f40cb1af4bdd0a010b", [:mix], [], "hexpm", "8663444fa85101a117887c170204d4c5a2182567e5f84767f0071cf15f2efb1e"}, } diff --git a/examples/plug_app/mix.exs b/examples/plug_app/mix.exs index 2586c731..4406698e 100644 --- a/examples/plug_app/mix.exs +++ b/examples/plug_app/mix.exs @@ -29,9 +29,8 @@ defmodule PlugApp.Mixfile do {:plug, "~> 1.0"}, {:ecto, "~> 3.11"}, {:ecto_sqlite3, "~> 0.16"}, - {:jason, "~> 1.0"} - # {:dep_from_hexpm, "~> 0.3.0"}, - # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, + {:jason, "~> 1.0"}, + {:ymlr, "~> 5.0"} ] end @@ -44,7 +43,9 @@ defmodule PlugApp.Mixfile do setup: ["deps.get", "ecto.setup"], "ecto.setup": ["ecto.create", "ecto.migrate"], "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], + spec: ["openapi.spec.json --spec PlugApp.ApiSpec \"priv/static/swagger.json\""], + "spec.yaml": ["openapi.spec.yaml --spec PlugApp.ApiSpec \"priv/static/swagger.yaml\""] ] end end diff --git a/examples/plug_app/mix.lock b/examples/plug_app/mix.lock index ee07bee3..f3fd4fba 100644 --- a/examples/plug_app/mix.lock +++ b/examples/plug_app/mix.lock @@ -1,27 +1,28 @@ %{ "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, - "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"}, - "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.18.1", "63c4b01cbb67f5e7eecde6819bc050fbb835141b534b779f80f40d479cdce669", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "ce223f0eda408437e7dc2b5f2a9dab306c7a709dc865f96172ba5eadfea3ce48"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esqlite": {:hex, :esqlite, "0.4.1", "ba5d0bab6b9c8432ffe1bf12fee8e154a50f1c3c40eadc3a9c870c23ca94d961", [:rebar3], [], "hexpm", "3584ca33172f4815ce56e96eed9835f5d8c987a9000fbc8c376c86acef8bf965"}, - "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"}, + "exqlite": {:hex, :exqlite, "0.29.0", "e6f1de4bfe3ce6e4c4260b15fef830705fa36632218dc7eafa0a5aba3a5d6e04", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a75f8a069fcdad3e5f95dfaddccd13c2112ea3b742fdcc234b96410e9c1bde00"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm", "ba952bfa35b374e1e5d84bc5f5efe8360c6f99dc93b3118f714a9a2dff6c9e19"}, "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.4.1", "a292b807c7670f1c2c136f7224934bf5929c998ea1b9ded63b710f3dd5140e4c", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.6", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm", "19358813676eb058d1b0630cd6914817e7dbf001022c49159510241f14f0eea7"}, "sqlitex": {:hex, :sqlitex, "1.7.1", "022d477aab2ae999c43ae6fbd1782ff1457e0e95c251c7b5fa6f7b7b102040ff", [:mix], [{:decimal, "~> 1.7", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm", "ef16cda37b151136a47a6c0830dc9eb5e5f8f5f029b649e9f3a58a6eed634b80"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "ymlr": {:hex, :ymlr, "5.1.3", "a8061add5a378e20272a31905be70209a5680fdbe0ad51f40cb1af4bdd0a010b", [:mix], [], "hexpm", "8663444fa85101a117887c170204d4c5a2182567e5f84767f0071cf15f2efb1e"}, } From fa34dd00a056ee370554238d7eb03b311f210819 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Sun, 25 May 2025 16:20:45 +0300 Subject: [PATCH 05/23] Fix 1.18 compilation warnings (#665) * Fix Elixir 1.18 compilation warnings * Test against 1.18 --- .github/workflows/elixir.yml | 9 ++++----- lib/open_api_spex/deprecated_cast.ex | 2 +- test/open_api/decode_test.exs | 4 +++- test/schema_construction_test.exs | 4 +--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 19c61bdc..51bc71b2 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -61,20 +61,19 @@ jobs: run: mix do deps.get, test test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 name: Test (OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}) strategy: matrix: - otp: ["23", "24", "25", "26"] - elixir: ["1.14", "1.15", "1.16"] + otp: ["24", "25", "26"] + elixir: ["1.14", "1.15", "1.16", "1.18"] # Test each elixir version with lowest and highest compatible OTP version, exclude others # See https://hexdocs.pm/elixir/compatibility-and-deprecations.html#between-elixir-and-erlang-otp exclude: - { otp: "24", elixir: "1.14" } + - { otp: "24", elixir: "1.18" } - { otp: "25", elixir: "1.14" } - - { otp: "23", elixir: "1.15" } - { otp: "25", elixir: "1.15" } - - { otp: "23", elixir: "1.16" } - { otp: "25", elixir: "1.16" } steps: - uses: actions/checkout@v4 diff --git a/lib/open_api_spex/deprecated_cast.ex b/lib/open_api_spex/deprecated_cast.ex index 7606b11e..28d21cdd 100644 --- a/lib/open_api_spex/deprecated_cast.ex +++ b/lib/open_api_spex/deprecated_cast.ex @@ -156,7 +156,7 @@ defmodule OpenApiSpex.DeprecatedCast do defp make_struct(val, %{"x-struct": nil}), do: {:ok, val} defp make_struct(val, %{"x-struct": mod}) do - keys = mod.schema |> OpenApiSpex.Schema.properties() |> Keyword.keys() + keys = mod.schema() |> OpenApiSpex.Schema.properties() |> Keyword.keys() if Map.keys(val) -- keys == [] do {:ok, struct(mod, val)} diff --git a/test/open_api/decode_test.exs b/test/open_api/decode_test.exs index 41a1f85d..bcef54d2 100644 --- a/test/open_api/decode_test.exs +++ b/test/open_api/decode_test.exs @@ -1,6 +1,8 @@ defmodule OpenApiSpex.OpenApi.DecodeTest do use ExUnit.Case - use Plug.Test + + import Plug.Test + import Plug.Conn alias OpenApiSpex.OpenApi diff --git a/test/schema_construction_test.exs b/test/schema_construction_test.exs index d7559b0e..107e67c6 100644 --- a/test/schema_construction_test.exs +++ b/test/schema_construction_test.exs @@ -48,9 +48,7 @@ defmodule OpenApiSpex.SchemaConstructionTest do end test "able to define schema without defining a struct" do - assert_raise UndefinedFunctionError, fn -> - SchemaWithoutStructDef.__struct__() - end + refute function_exported?(SchemaWithoutStructDef, :__struct__, 0) end defmodule SchemaWithoutDerive do From 22f46f527c3357b426f3786c68dd772ba2b67a1e Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Mon, 26 May 2025 13:28:01 +0300 Subject: [PATCH 06/23] Check for ex_doc warnings in CI and bump devtest deps (#666) --- .github/workflows/elixir.yml | 3 +++ mix.lock | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 51bc71b2..f8dee5a7 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -35,6 +35,7 @@ jobs: - run: mix format --check-formatted - run: mix credo --strict --all - run: mix dialyzer --format github + - run: mix docs --warnings-as-errors test_examples: runs-on: ubuntu-latest @@ -44,6 +45,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 + id: beam with: otp-version: 26 elixir-version: 1.16 @@ -78,6 +80,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 + id: beam with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} diff --git a/mix.lock b/mix.lock index 53da2382..ea52314c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,11 +1,11 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, + "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, From 410f3aaa64b9abcb7f56de91e8aa2d90cf356f66 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Mon, 26 May 2025 16:14:26 +0300 Subject: [PATCH 07/23] Test array query params in example phoenix app (#667) --- .../lib/phoenix_app/accounts/accounts.ex | 15 +++++++++++-- .../controllers/user_controller.ex | 19 +++++++++++------ .../user_controller_with_struct_specs.ex | 21 +++++++++++++------ .../controllers/user_controller_test.exs | 6 +++--- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/examples/phoenix_app/lib/phoenix_app/accounts/accounts.ex b/examples/phoenix_app/lib/phoenix_app/accounts/accounts.ex index a7d6f29f..b1ff6d9d 100644 --- a/examples/phoenix_app/lib/phoenix_app/accounts/accounts.ex +++ b/examples/phoenix_app/lib/phoenix_app/accounts/accounts.ex @@ -2,8 +2,19 @@ defmodule PhoenixApp.Accounts do alias PhoenixApp.Accounts.User alias PhoenixApp.Repo - def list_users() do - Repo.all(User) + import Ecto.Query + + def list_users(filters) do + query = from(u in User, order_by: [asc: u.id]) + + query = + if filters[:ids] do + from(u in query, where: u.id in ^filters[:ids]) + else + query + end + + Repo.all(query) end def get_user!(id) do diff --git a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex index afbde22d..f8b8d59c 100644 --- a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex +++ b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex @@ -21,13 +21,22 @@ defmodule PhoenixAppWeb.UserController do operation :index, summary: "List users", description: "List all users", + parameters: [ + OpenApiSpex.Operation.parameter( + :ids, + :query, + %Schema{type: :array, items: %Schema{type: :integer}}, + "Filter by user ids" + ) + ], responses: [ ok: {"User List Response", "application/json", Schemas.UsersResponse}, unprocessable_entity: %Reference{"$ref": "#/components/responses/unprocessable_entity"} ] - def index(conn, _params) do - users = Accounts.list_users() + def index(conn, params) do + users = Accounts.list_users(params) + render(conn, "index.json", users: users) end @@ -37,8 +46,7 @@ defmodule PhoenixAppWeb.UserController do parameters: [ group_id: [in: :path, type: :integer, description: "Group ID", example: 1] ], - request_body: - {"The user attributes", "application/json", Schemas.UserRequest, required: true}, + request_body: {"The user attributes", "application/json", Schemas.UserRequest, required: true}, responses: [ created: {"User", "application/json", Schemas.UserResponse} ] @@ -88,8 +96,7 @@ defmodule PhoenixAppWeb.UserController do required: true ] ], - request_body: - {"The user attributes", "application/json", Schemas.UserRequest, required: true}, + request_body: {"The user attributes", "application/json", Schemas.UserRequest, required: true}, responses: [ ok: {"User", "application/json", Schemas.UserResponse} ] diff --git a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller_with_struct_specs.ex b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller_with_struct_specs.ex index 74bc3107..b5a958b5 100644 --- a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller_with_struct_specs.ex +++ b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller_with_struct_specs.ex @@ -4,8 +4,10 @@ defmodule PhoenixAppWeb.UserControllerWithStructSpecs do of using the ExDoc-based operation specs. """ use PhoenixAppWeb, :controller + import OpenApiSpex.Operation, only: [parameter: 5, request_body: 4, response: 3] - alias OpenApiSpex.{Operation, Reference} + + alias OpenApiSpex.{Operation, Reference, Schema} alias PhoenixApp.{Accounts, Accounts.User} alias PhoenixAppWeb.Schemas @@ -34,6 +36,14 @@ defmodule PhoenixAppWeb.UserControllerWithStructSpecs do summary: "List users", description: "List all useres", operationId: "UserController.index", + parameters: [ + OpenApiSpex.Operation.parameter( + :ids, + :query, + %Schema{type: :array, items: %Schema{type: :integer}}, + "Filter by user ids" + ) + ], responses: %{ 200 => response("User List Response", "application/json", Schemas.UsersResponse), 422 => %Reference{"$ref": "#/components/responses/unprocessable_entity"} @@ -41,8 +51,9 @@ defmodule PhoenixAppWeb.UserControllerWithStructSpecs do } end - def index(conn, _params) do - users = Accounts.list_users() + def index(conn, params) do + users = Accounts.list_users(params) + render(conn, "index.json", users: users) end @@ -56,9 +67,7 @@ defmodule PhoenixAppWeb.UserControllerWithStructSpecs do Operation.parameter(:group_id, :path, :integer, "Group ID", example: 1) ], requestBody: - request_body("The user attributes", "application/json", Schemas.UserRequest, - required: true - ), + request_body("The user attributes", "application/json", Schemas.UserRequest, required: true), responses: %{ 201 => response("User", "application/json", Schemas.UserResponse) } diff --git a/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs b/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs index d5a085ef..af782c99 100644 --- a/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs +++ b/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs @@ -34,17 +34,17 @@ defmodule PhoenixAppWeb.UserControllerTest do %{id: id2} = PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Benjamin", email: "ben@lycos.com"}) - %{id: id3} = + %{id: _id3} = PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Chuck", email: "chuck@aol.com"}) response = conn |> Plug.Conn.put_req_header("accept", "application/json") - |> get(user_path(conn, :index)) + |> get(user_path(conn, :index), ids: [id1, id2]) |> json_response(200) |> assert_schema("UsersResponse", spec) - assert [%{id: ^id1}, %{id: ^id2}, %{id: ^id3}] = response.data + assert [%{id: ^id1}, %{id: ^id2}] = response.data end test "User schema example", %{spec: spec} do From 2069321f6a31b2c4a09a2eba561868dc7a5b876b Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Wed, 25 Jun 2025 15:07:34 +0300 Subject: [PATCH 08/23] Release 3.21.3 --- CHANGELOG.md | 6 ++++++ mix.exs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a10fe5cf..726b6c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v3.21.3 - 2025-06-25 + +* Fix cast x-validate when decoded schema by @GPrimola in https://github.com/open-api-spex/open_api_spex/pull/647 +* Add examples property to Schema by @madjar in https://github.com/open-api-spex/open_api_spex/pull/654 +* Document schema resolver duplicate titles behaviour by @zorbash in https://github.com/open-api-spex/open_api_spex/pull/656 +* Fix 1.18 compilation warnings by @zorbash in https://github.com/open-api-spex/open_api_spex/pull/665 ## v3.21.2 - 2024-10-02 diff --git a/mix.exs b/mix.exs index e554052e..15f2db4c 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.21.2" + @version "3.21.3" def project do [ From 572130d1cf0eb0af23da635b5e022fa2d8e9efad Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Tue, 1 Jul 2025 20:35:38 +0300 Subject: [PATCH 09/23] Fix OTP-28 support (#672) * Refactor: Update default parsers and schema pattern definitions This commit introduces changes to `OpenApiSpex.CastParameters` and `OpenApiSpexTest.Schemas`. - Moved `@default_parsers` to a private function `default_parsers/0` in `OpenApiSpex.CastParameters` to ensure it's evaluated at runtime, preventing potential compilation issues with `OpenApi.json_encoder()`. - Updated the `pattern` definition in `OpenApiSpexTest.Schemas` to use a string literal instead of a regex literal for consistency and to avoid potential issues with regex compilation. Fix: Adjust string pattern test assertion Following the refactoring, a test in `OpenApiSpex.CastStringTest` failed due to a change in how regex patterns were handled. This fix corrects the assertion for string pattern matching to compare the `source` of the regex instead of the regex struct directly, resolving the test failure. * Fix formatting --------- Co-authored-by: Dimitris Zorbas --- lib/open_api_spex/cast_parameters.ex | 7 ++++--- lib/open_api_spex/controller_specs.ex | 4 +++- test/cast/string_test.exs | 2 +- test/support/schemas.ex | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/open_api_spex/cast_parameters.ex b/lib/open_api_spex/cast_parameters.ex index 1155cb79..9ec6f92b 100644 --- a/lib/open_api_spex/cast_parameters.ex +++ b/lib/open_api_spex/cast_parameters.ex @@ -4,8 +4,6 @@ defmodule OpenApiSpex.CastParameters do alias OpenApiSpex.Cast.Error alias Plug.Conn - @default_parsers %{~r/^application\/.*json.*$/ => OpenApi.json_encoder()} - @spec cast(Plug.Conn.t(), Operation.t(), OpenApi.t(), opts :: [OpenApiSpex.cast_opt()]) :: {:error, [Error.t()]} | {:ok, Conn.t()} def cast(conn, operation, spec, opts \\ []) do @@ -120,7 +118,7 @@ defmodule OpenApiSpex.CastParameters do opts ) do parsers = Map.get(ext || %{}, "x-parameter-content-parsers", %{}) - parsers = Map.merge(@default_parsers, parsers) + parsers = Map.merge(default_parsers(), parsers) conn |> get_params_by_location( @@ -134,6 +132,9 @@ defmodule OpenApiSpex.CastParameters do end end + defp default_parsers, + do: %{~r/^application\/.*json.*$/ => OpenApi.json_encoder()} + defp pre_parse_parameters(%{} = parameters, %{} = parameters_context, parsers) do Enum.reduce_while(parameters, Map.new(), fn {key, value}, acc -> case pre_parse_parameter(value, Map.get(parameters_context, key, %{}), parsers) do diff --git a/lib/open_api_spex/controller_specs.ex b/lib/open_api_spex/controller_specs.ex index 144843e0..d1595579 100644 --- a/lib/open_api_spex/controller_specs.ex +++ b/lib/open_api_spex/controller_specs.ex @@ -386,7 +386,9 @@ defmodule OpenApiSpex.ControllerSpecs do extensions = spec - |> Enum.filter(fn {key, _val} -> is_atom(key) && String.starts_with?(to_string(key), "x-") end) + |> Enum.filter(fn {key, _val} -> + is_atom(key) && String.starts_with?(to_string(key), "x-") + end) |> Map.new(fn {key, value} -> {to_string(key), value} end) %Operation{ diff --git a/test/cast/string_test.exs b/test/cast/string_test.exs index 427c616a..eabb5703 100644 --- a/test/cast/string_test.exs +++ b/test/cast/string_test.exs @@ -27,7 +27,7 @@ defmodule OpenApiSpex.CastStringTest do assert {:error, [error]} = cast(value: "hello", schema: schema) assert error.reason == :invalid_format assert error.value == "hello" - assert error.format == ~r/\d-\d/ + assert error.format.source == "\\d-\\d" end test "string with format (date time)" do diff --git a/test/support/schemas.ex b/test/support/schemas.ex index 65f687fd..0f93748f 100644 --- a/test/support/schemas.ex +++ b/test/support/schemas.ex @@ -186,7 +186,7 @@ defmodule OpenApiSpexTest.Schemas do 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_]+/}, + name: %Schema{type: :string, description: "User name", pattern: "[a-zA-Z][a-zA-Z0-9_]+"}, email: %Schema{type: :string, description: "Email address", format: :email}, password: %Schema{type: :string, description: "Login password", writeOnly: true}, age: %Schema{type: :integer, description: "Age"}, From 61d5951dc49410d6302039944c6e0c56b11dd0ce Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Tue, 1 Jul 2025 20:39:04 +0300 Subject: [PATCH 10/23] Release version 3.21.4 --- CHANGELOG.md | 4 ++++ mix.exs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 726b6c89..5b4c7e51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v3.21.4 - 2025-07-01 + +* Fix OTP-28 support by @bopm in https://github.com/open-api-spex/open_api_spex/pull/672 + ## v3.21.3 - 2025-06-25 * Fix cast x-validate when decoded schema by @GPrimola in https://github.com/open-api-spex/open_api_spex/pull/647 diff --git a/mix.exs b/mix.exs index 15f2db4c..c69afc5e 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.21.3" + @version "3.21.4" def project do [ From c71c312d0d8a72b92624e4deb67efb8cca385264 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Tue, 8 Jul 2025 18:58:49 +0300 Subject: [PATCH 11/23] Fix assert_operation_response/2 references (#673) * Support references in assert_operation_response/2 * fixup! Support references in assert_operation_response/2 --- lib/open_api_spex/test/test_assertions.ex | 11 ++++++++++- test/support/api_spec.ex | 2 +- test/support/pet_controller.ex | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/open_api_spex/test/test_assertions.ex b/lib/open_api_spex/test/test_assertions.ex index 809044d6..a08a37c0 100644 --- a/lib/open_api_spex/test/test_assertions.ex +++ b/lib/open_api_spex/test/test_assertions.ex @@ -166,8 +166,17 @@ defmodule OpenApiSpex.TestAssertions do Map.get(responses, code_range) || Map.get(responses, :"#{code_range}", %{}) + resolved_response = + case response do + %OpenApiSpex.Reference{} = ref -> + OpenApiSpex.Reference.resolve_response(ref, spec.components.responses) + + _ -> + response + end + resolved_schema = - response + resolved_response |> Map.get(:content, %{}) |> Map.get(content_type, %{}) |> Map.get(:schema) diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex index 5c2629af..ec59dda1 100644 --- a/test/support/api_spec.ex +++ b/test/support/api_spec.ex @@ -76,7 +76,7 @@ defmodule OpenApiSpexTest.ApiSpec do } }, responses: %{ - unprocessable_entity: %Response{ + "unprocessable_entity" => %Response{ description: "Unprocessable Entity", content: %{"application/json" => %MediaType{schema: %Schema{type: :object}}} } diff --git a/test/support/pet_controller.ex b/test/support/pet_controller.ex index 9b5e7150..8ae0474a 100644 --- a/test/support/pet_controller.ex +++ b/test/support/pet_controller.ex @@ -47,7 +47,9 @@ defmodule OpenApiSpexTest.PetController do ], responses: [ ok: {"Pet list", "application/json", Schemas.PetsResponse}, - unprocessable_entity: OpenApiSpex.JsonErrorResponse.response() + unprocessable_entity: %OpenApiSpex.Reference{ + "$ref": "#/components/responses/unprocessable_entity" + } ], operation_id: "listPets" def index(conn, _params) do From bc1606b9e55c959a5cd09ba93d236837fe342ca7 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Tue, 8 Jul 2025 19:23:50 +0300 Subject: [PATCH 12/23] Release version 3.21.5 --- CHANGELOG.md | 4 ++++ mix.exs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b4c7e51..019f50b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v3.21.5 - 2025-07-08 + +* Fix assert_operation_response/2 references by @zorbash in https://github.com/open-api-spex/open_api_spex/pull/673 + ## v3.21.4 - 2025-07-01 * Fix OTP-28 support by @bopm in https://github.com/open-api-spex/open_api_spex/pull/672 diff --git a/mix.exs b/mix.exs index c69afc5e..c47a8444 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.21.4" + @version "3.21.5" def project do [ From 9967e2f9d8231655dec902ac2258a04b19e30ae8 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Wed, 30 Jul 2025 12:24:56 +0300 Subject: [PATCH 13/23] Support multiple apps in Plug.SwaggerUI (#676) --- lib/open_api_spex/plug/swagger_ui.ex | 40 ++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/open_api_spex/plug/swagger_ui.ex b/lib/open_api_spex/plug/swagger_ui.ex index 9fb72986..0ffdbabf 100644 --- a/lib/open_api_spex/plug/swagger_ui.ex +++ b/lib/open_api_spex/plug/swagger_ui.ex @@ -41,6 +41,19 @@ defmodule OpenApiSpex.Plug.SwaggerUI do swagger_ui_js_standalone_preset_url: "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.14.0/swagger-ui-standalone-preset.js", swagger_ui_css_url: "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.14.0/swagger-ui.css" end + + # Multiple paths + scope "/" do + pipe_through :browser # Use the default browser stack + + get "/", MyAppWeb.PageController, :index + get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, + paths: [ + latest: "/api/openapi", + legacy: "/legacy_api/openapi", + other_app: "http://localhost:4001/my_other_app/api/openapi" + ] + end """ @behaviour Plug @@ -91,10 +104,8 @@ defmodule OpenApiSpex.Plug.SwaggerUI do window.onload = function() { // Begin Swagger UI call region const api_spec_url = new URL(window.location); - api_spec_url.pathname = "<%= config.path %>"; - api_spec_url.hash = ""; - const ui = SwaggerUIBundle({ - url: api_spec_url.href, + + let swaggerConfig = { dom_id: '#swagger-ui', deepLinking: true, presets: [ @@ -117,7 +128,26 @@ defmodule OpenApiSpex.Plug.SwaggerUI do <%= for {k, v} <- Map.drop(config, [:path, :oauth, :csp_nonce_assign_key]) do %> , <%= camelize(k) %>: <%= encode_config(camelize(k), v) %> <% end %> - }) + }; + + <%= if config[:paths] do %> + let urls = <%= + config[:paths] + |> Enum.map(fn {name, path} -> %{name: name, url: path} end) + |> OpenApiSpex.OpenApi.json_encoder().encode!() + %> + let urlconfig = { urls: urls }; + + swaggerConfig = { ...swaggerConfig, ...urlconfig}; + <% else %> + api_spec_url.pathname = "<%= config.path %>"; + api_spec_url.hash = ""; + let urlConfig = { url: api_spec_url.href }; + + swaggerConfig = {...swaggerConfig, ...urlConfig }; + <% end %> + const ui = SwaggerUIBundle(swaggerConfig); + // End Swagger UI call region <%= if config[:oauth] do %> ui.initOAuth( From 7de53cb763519b76ae75583685bfbf99b51404a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pavl=C3=ADk?= Date: Thu, 31 Jul 2025 17:25:52 +0200 Subject: [PATCH 14/23] Validate keys given to operation/2 macro (#675) --- lib/open_api_spex/controller_specs.ex | 43 +++++++++++++++++++++++++-- test/controller_specs_test.exs | 36 +++++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/lib/open_api_spex/controller_specs.ex b/lib/open_api_spex/controller_specs.ex index d1595579..5578e089 100644 --- a/lib/open_api_spex/controller_specs.ex +++ b/lib/open_api_spex/controller_specs.ex @@ -366,15 +366,50 @@ defmodule OpenApiSpex.ControllerSpecs do end end + @operation_permitted_keys [ + :callbacks, + :description, + :deprecated, + :external_docs, + :operation_id, + :parameters, + :request_body, + :responses, + :security, + :summary, + :tags + ] + @doc """ Define an Operation for a controller action. See `OpenApiSpex.ControllerSpecs` for usage and examples. + + Permitted operation keys: + + #{Enum.map_join(@operation_permitted_keys, "\n", fn k -> "* `#{k}`" end)} + * keys prefixed by `x-` for extensions """ def operation_spec(_module, _action, nil = _spec), do: nil def operation_spec(_module, _action, false = _spec), do: nil def operation_spec(module, action, spec) do + validation_result = + spec + |> Enum.reject(fn {key, _val} -> extension_key?(key) end) + |> Keyword.validate(@operation_permitted_keys) + + case validation_result do + {:ok, _spec} -> + :ok + + {:error, unknown_keys} -> + raise ArgumentError, + "Unknown keys given to operation/2: #{inspect(unknown_keys)}. " <> + "Allowed keys are: #{inspect(@operation_permitted_keys)}, " <> + "and keys starting with 'x-'." + end + spec = Map.new(spec) shared_tags = Module.get_attribute(module, :shared_tags, []) |> List.flatten() @@ -386,9 +421,7 @@ defmodule OpenApiSpex.ControllerSpecs do extensions = spec - |> Enum.filter(fn {key, _val} -> - is_atom(key) && String.starts_with?(to_string(key), "x-") - end) + |> Enum.filter(fn {key, _val} -> extension_key?(key) end) |> Map.new(fn {key, value} -> {to_string(key), value} end) %Operation{ @@ -406,4 +439,8 @@ defmodule OpenApiSpex.ControllerSpecs do extensions: extensions } end + + defp extension_key?(key) do + is_atom(key) && String.starts_with?(to_string(key), "x-") + end end diff --git a/test/controller_specs_test.exs b/test/controller_specs_test.exs index 7ef0f786..eff3b1de 100644 --- a/test/controller_specs_test.exs +++ b/test/controller_specs_test.exs @@ -7,7 +7,7 @@ defmodule OpenApiSpex.ControllerSpecsTest do alias OpenApiSpexTest.DslController alias OpenApiSpexTest.DslControllerOperationStructs - describe "operation/1" do + describe "operation/2" do test "supports :parameters" do assert %OpenApiSpex.Operation{ responses: %{}, @@ -157,5 +157,39 @@ defmodule OpenApiSpex.ControllerSpecsTest do assert %OpenApiSpex.Operation{extensions: %{"x-foo" => "bar"}} = DslController.open_api_operation(:index) end + + test "raises when unknown key is provided" do + msg = + "Unknown keys given to operation/2: [:unknown]. Allowed keys are: " <> + "[:callbacks, :description, :deprecated, :external_docs, :operation_id, :parameters, " <> + ":request_body, :responses, :security, :summary, :tags], and keys starting with 'x-'." + + assert_raise ArgumentError, msg, fn -> + Code.eval_string(""" + defmodule TestController do + use OpenApiSpex.ControllerSpecs + + operation :index, + summary: "Users index", + parameters: [ + username: [ + in: :query, + description: "Filter by username", + type: :string + ] + ], + responses: [ + ok: {"Users index response", "application/json", UsersIndexResponse} + ], + unknown: "value", + "x-foo": "bar" + + def index(conn, _) do + json(conn, []) + end + end + """) + end + end end end From be4f59a3d00c2806ed470eb0bd5e5029f58b7de8 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Tue, 5 Aug 2025 14:59:19 +0300 Subject: [PATCH 15/23] Release version 3.22.0 --- CHANGELOG.md | 5 +++++ mix.exs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 019f50b2..40d71cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v3.22.0 - 2025-08-05 + +* Support multiple apps in Plug.SwaggerUI by @zorbash in https://github.com/open-api-spex/open_api_spex/pull/676 +* Validate keys given to operation/2 macro by @xxdavid in https://github.com/open-api-spex/open_api_spex/pull/675 + ## v3.21.5 - 2025-07-08 * Fix assert_operation_response/2 references by @zorbash in https://github.com/open-api-spex/open_api_spex/pull/673 diff --git a/mix.exs b/mix.exs index c47a8444..a4385068 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.21.5" + @version "3.22.0" def project do [ From aaee4a7f9a6c4c0d2d3f69e4ab053ca65fff60fb Mon Sep 17 00:00:00 2001 From: "Adam C. Stephens" <2071575+adamcstephens@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:54:40 -0500 Subject: [PATCH 16/23] Fix elixir 1.19 support (#685) --- lib/open_api_spex/inspect/for_schema.ex | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/open_api_spex/inspect/for_schema.ex b/lib/open_api_spex/inspect/for_schema.ex index a41bcafe..c217f59e 100644 --- a/lib/open_api_spex/inspect/for_schema.ex +++ b/lib/open_api_spex/inspect/for_schema.ex @@ -14,6 +14,16 @@ defimpl Inspect, for: OpenApiSpex.Schema do Map.has_key?(map, field), do: info - Inspect.Map.inspect(map, "OpenApiSpex.Schema", infos, opts) + do_inspect(map, "OpenApiSpex.Schema", infos, opts) + end + + if Version.compare(System.version(), "1.19.0") in [:gt, :eq] do + defp do_inspect(map, schema_mod, infos, opts) do + Inspect.Map.inspect_as_struct(map, schema_mod, infos, opts) + end + else + defp do_inspect(map, schema_mod, infos, opts) do + Inspect.Map.inspect(map, schema_mod, infos, opts) + end end end From 32a7af03fda86b345823d44be7c1655a6bb7a150 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Fri, 21 Nov 2025 18:12:38 +0200 Subject: [PATCH 17/23] Release version 3.22.1 --- CHANGELOG.md | 4 ++++ mix.exs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40d71cec..cbb69223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v3.22.1 - 2025-11-21 + +* Fix elixir 1.19 support by @adamcstephens in https://github.com/open-api-spex/open_api_spex/pull/685 + ## v3.22.0 - 2025-08-05 * Support multiple apps in Plug.SwaggerUI by @zorbash in https://github.com/open-api-spex/open_api_spex/pull/676 diff --git a/mix.exs b/mix.exs index a4385068..87226087 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.22.0" + @version "3.22.1" def project do [ From 1519adf0d630848f1563912a883cb05a5a5d9650 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Wed, 31 Dec 2025 04:21:24 -0500 Subject: [PATCH 18/23] fix: type warnings Elixir 1.19 (#693) Thanks @davydog187 ! --- lib/open_api_spex/cast/all_of.ex | 4 ++-- lib/open_api_spex/cast/any_of.ex | 4 ++-- lib/open_api_spex/cast/one_of.ex | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/open_api_spex/cast/all_of.ex b/lib/open_api_spex/cast/all_of.ex index e0f49ced..a71e977f 100644 --- a/lib/open_api_spex/cast/all_of.ex +++ b/lib/open_api_spex/cast/all_of.ex @@ -6,7 +6,7 @@ defmodule OpenApiSpex.Cast.AllOf do def cast(ctx), do: cast_all_of(ctx, nil) - defp cast_all_of(%{schema: %{allOf: [%Schema{} = schema | remaining]}} = ctx, acc) do + defp cast_all_of(%Cast{schema: %{allOf: [%Schema{} = schema | remaining]}} = ctx, acc) do relaxed_schema = %{schema | "x-struct": nil} new_ctx = put_in(ctx.schema.allOf, remaining) @@ -23,7 +23,7 @@ defmodule OpenApiSpex.Cast.AllOf do {:error, errors} -> Cast.error( - %Cast{ctx | errors: ctx.errors ++ errors}, + %{ctx | errors: ctx.errors ++ errors}, {:all_of, to_string(relaxed_schema.title || relaxed_schema.type)} ) end diff --git a/lib/open_api_spex/cast/any_of.ex b/lib/open_api_spex/cast/any_of.ex index 334cf0a2..9d870a05 100644 --- a/lib/open_api_spex/cast/any_of.ex +++ b/lib/open_api_spex/cast/any_of.ex @@ -12,7 +12,7 @@ defmodule OpenApiSpex.Cast.AnyOf do end defp cast_any_of( - %{schema: %{anyOf: [%Schema{} = schema | remaining]}} = ctx, + %Cast{schema: %{anyOf: [%Schema{} = schema | remaining]}} = ctx, failed_schemas, acc ) do @@ -32,7 +32,7 @@ defmodule OpenApiSpex.Cast.AnyOf do {:error, errors} -> cast_any_of( - %Cast{new_ctx | errors: new_ctx.errors ++ errors}, + %{new_ctx | errors: new_ctx.errors ++ errors}, [schema | failed_schemas], acc ) diff --git a/lib/open_api_spex/cast/one_of.ex b/lib/open_api_spex/cast/one_of.ex index 9fa37b53..fc4fdac8 100644 --- a/lib/open_api_spex/cast/one_of.ex +++ b/lib/open_api_spex/cast/one_of.ex @@ -8,7 +8,7 @@ defmodule OpenApiSpex.Cast.OneOf do error(ctx, [], []) end - def cast(%{schema: %{type: _, oneOf: schemas}} = ctx) do + def cast(%Cast{schema: %{type: _, oneOf: schemas}} = ctx) do castable_schemas = Enum.reduce(schemas, {ctx, [], []}, fn schema, {ctx, results, error_schemas} -> schema = OpenApiSpex.resolve_schema(schema, ctx.schemas) @@ -19,7 +19,7 @@ defmodule OpenApiSpex.Cast.OneOf do {ctx, [{:ok, value, schema} | results], error_schemas} else {:error, errors} -> - {%Cast{ctx | errors: ctx.errors ++ errors}, results, [schema | error_schemas]} + {%{ctx | errors: ctx.errors ++ errors}, results, [schema | error_schemas]} end end) From f2c71bf320045b76c4bc2ea9a7a056c8d9092197 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Thu, 8 Jan 2026 14:10:01 +0200 Subject: [PATCH 19/23] Release version 3.22.2 --- CHANGELOG.md | 4 ++++ mix.exs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb69223..b4a35451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v3.22.2 - 2026-01-08 + +* fix: type warnings Elixir 1.19 by @davydog187 in https://github.com/open-api-spex/open_api_spex/pull/693 + ## v3.22.1 - 2025-11-21 * Fix elixir 1.19 support by @adamcstephens in https://github.com/open-api-spex/open_api_spex/pull/685 diff --git a/mix.exs b/mix.exs index 87226087..d07824c2 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.22.1" + @version "3.22.2" def project do [ From 224d1e1c3454c88aca57fdfd567983d113d9826a Mon Sep 17 00:00:00 2001 From: David-Klemenc <44705886+David-Klemenc@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:05:18 +0200 Subject: [PATCH 20/23] chore: removed unused require(s) (#700) --- lib/mix/tasks/openapi.spec.json.ex | 1 - lib/mix/tasks/openapi.spec.yaml.ex | 1 - lib/open_api_spex/export_spec.ex | 1 - 3 files changed, 3 deletions(-) diff --git a/lib/mix/tasks/openapi.spec.json.ex b/lib/mix/tasks/openapi.spec.json.ex index f50cd849..51a1eb50 100644 --- a/lib/mix/tasks/openapi.spec.json.ex +++ b/lib/mix/tasks/openapi.spec.json.ex @@ -29,7 +29,6 @@ defmodule Mix.Tasks.Openapi.Spec.Json do * `--filename` - The output filename (defaults to "#{@default_filename}") """ use Mix.Task - require Mix.Generator @impl true def run(argv) do diff --git a/lib/mix/tasks/openapi.spec.yaml.ex b/lib/mix/tasks/openapi.spec.yaml.ex index 5c4c2289..f64dc467 100644 --- a/lib/mix/tasks/openapi.spec.yaml.ex +++ b/lib/mix/tasks/openapi.spec.yaml.ex @@ -26,7 +26,6 @@ defmodule Mix.Tasks.Openapi.Spec.Yaml do * `--filename` - The output filename (defaults to "#{@default_filename}") """ use Mix.Task - require Mix.Generator @dialyzer {:nowarn_function, encoder: 0} diff --git a/lib/open_api_spex/export_spec.ex b/lib/open_api_spex/export_spec.ex index 4e8c1ff7..60d9a42f 100644 --- a/lib/open_api_spex/export_spec.ex +++ b/lib/open_api_spex/export_spec.ex @@ -2,7 +2,6 @@ defmodule OpenApiSpex.ExportSpec do @moduledoc """ Export spec in specified encoding """ - require Mix.Generator defmodule Options do @moduledoc false From 1377df0177133a0177f901e9ff5e859d878374a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 5 May 2026 18:47:04 +0200 Subject: [PATCH 21/23] Relax decimal requirement (#702) A new release will be out soon, this will allow users to migrate if desired. The new version is compatible with OpenAPISpex. --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index d07824c2..f16abed7 100644 --- a/mix.exs +++ b/mix.exs @@ -67,7 +67,7 @@ defmodule OpenApiSpex.Mixfile do {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:jason, "~> 1.0", optional: true}, - {:decimal, "~> 1.0 or ~> 2.0", optional: true}, + {:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", optional: true}, {:phoenix, "~> 1.3", only: [:dev, :test]}, {:plug, "~> 1.7"}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", optional: true}, From 8850f32e72f978a40043d7fdeec604da2df49b00 Mon Sep 17 00:00:00 2001 From: Dimitris Zorbas Date: Tue, 5 May 2026 19:51:35 +0300 Subject: [PATCH 22/23] Release version 3.22.3 --- CHANGELOG.md | 5 +++++ mix.exs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a35451..b7d37589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v3.22.3 - 2026-05-05 + +* chore: removed unused require(s) by @David-Klemenc in https://github.com/open-api-spex/open_api_spex/pull/700 +* Relax decimal requirement by @josevalim in https://github.com/open-api-spex/open_api_spex/pull/702 + ## v3.22.2 - 2026-01-08 * fix: type warnings Elixir 1.19 by @davydog187 in https://github.com/open-api-spex/open_api_spex/pull/693 diff --git a/mix.exs b/mix.exs index f16abed7..8b5b67cb 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.22.2" + @version "3.22.3" def project do [ From b2ed58b6610cc76a2d6d0f893765c037d02e3ab8 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Tue, 7 Jul 2026 05:50:10 +1200 Subject: [PATCH 23/23] Make layout configurable in Swagger UI (#704) --- lib/open_api_spex/plug/swagger_ui.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/open_api_spex/plug/swagger_ui.ex b/lib/open_api_spex/plug/swagger_ui.ex index 0ffdbabf..833f8bc4 100644 --- a/lib/open_api_spex/plug/swagger_ui.ex +++ b/lib/open_api_spex/plug/swagger_ui.ex @@ -115,7 +115,7 @@ defmodule OpenApiSpex.Plug.SwaggerUI do plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], - layout: "StandaloneLayout", + layout: "<%= config[:layout] || "StandaloneLayout" %>", requestInterceptor: function(request){ server_base = window.location.protocol + "//" + window.location.host; if(request.url.startsWith(server_base)) {