From ab735d82c21afeea9a2e05292ea8496793711ff9 Mon Sep 17 00:00:00 2001 From: Diederick Lawson Date: Wed, 15 Jan 2025 15:29:24 +0100 Subject: [PATCH 1/2] Prefers the built-in JSON library from Elixir 1.18.x when it's available Unfortunately, the JSON module doesn't support the `pretty` option, so that will use either Jason or Poison to encode the API specs in the `openapi.spec.json` mix task. --- lib/mix/tasks/openapi.spec.json.ex | 6 +++++- lib/open_api_spex.ex | 8 ++++---- lib/open_api_spex/open_api.ex | 4 ++-- lib/open_api_spex/plug/render_spec.ex | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/mix/tasks/openapi.spec.json.ex b/lib/mix/tasks/openapi.spec.json.ex index f50cd849..ebb1244c 100644 --- a/lib/mix/tasks/openapi.spec.json.ex +++ b/lib/mix/tasks/openapi.spec.json.ex @@ -42,9 +42,13 @@ defmodule Mix.Tasks.Openapi.Spec.Json do defp maybe_start_app(true), do: Mix.Task.run("app.start") defp maybe_start_app(_), do: Mix.Task.run("app.config", preload_modules: true) + # Unfortunately, the built-in JSON module in Elixir 1.18.x doesn't support the `pretty` option + # So we need to take Jason or Poison to be able to support this feature + defp json_encoder(), do: Enum.find([Jason, Poison], &Code.ensure_loaded?/1) + defp encode(spec, %{pretty: pretty}) do spec - |> OpenApiSpex.OpenApi.json_encoder().encode(pretty: pretty) + |> json_encoder().encode(pretty: pretty) |> case do {:ok, json} -> "#{json}\n" diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex index a7d18c74..31e4bafe 100644 --- a/lib/open_api_spex.ex +++ b/lib/open_api_spex.ex @@ -181,7 +181,7 @@ defmodule OpenApiSpex do - ensures the schema is linked to the module by "x-struct" extension property - defines a struct with keys matching the schema properties - defines a @type `t` for the struct - - derives a `Jason.Encoder` and/or `Poison.Encoder` for the struct + - derives a `JSON.Encoder`, `Jason.Encoder` and/or `Poison.Encoder` for the struct See `OpenApiSpex.Schema` for additional examples and details. @@ -238,8 +238,8 @@ defmodule OpenApiSpex do - `:struct?` (boolean) - When false, prevents the automatic generation of a struct definition for the schema module. - `:derive?` (boolean) When false, prevents the automatic generation - of a `@derive` call for either `Poison.Encoder` - or `Jason.Encoder`. Using this option can + of a `@derive` call for either `JSON.Encoder` + `Poison.Encoder` or `Jason.Encoder`. Using this option can prevent "... protocol has already been consolidated ..." compiler warnings. """ @@ -262,7 +262,7 @@ defmodule OpenApiSpex do if Map.get(@schema, :"x-struct") == __MODULE__ do if Keyword.get(unquote(opts), :derive?, true) do - @derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1) + @derive Enum.filter([JSON.Encoder, Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1) end if Keyword.get(unquote(opts), :struct?, true) do diff --git a/lib/open_api_spex/open_api.ex b/lib/open_api_spex/open_api.ex index 8e8ee1bb..817606bd 100644 --- a/lib/open_api_spex/open_api.ex +++ b/lib/open_api_spex/open_api.ex @@ -70,7 +70,7 @@ defmodule OpenApiSpex.OpenApi do """ @callback spec() :: t - @json_encoder Enum.find([Jason, Poison], &Code.ensure_loaded?/1) + @json_encoder Enum.find([JSON, Jason, Poison], &Code.ensure_loaded?/1) @yaml_encoder nil @vendor_extensions ~w( x-struct @@ -80,7 +80,7 @@ defmodule OpenApiSpex.OpenApi do def json_encoder, do: @json_encoder - for encoder <- [Poison.Encoder, Jason.Encoder] do + for encoder <- [JSON.Encoder, Poison.Encoder, Jason.Encoder] do if Code.ensure_loaded?(encoder) do defimpl encoder do def encode(api_spec = %OpenApi{}, options) do diff --git a/lib/open_api_spex/plug/render_spec.ex b/lib/open_api_spex/plug/render_spec.ex index f07804d3..6a5dd559 100644 --- a/lib/open_api_spex/plug/render_spec.ex +++ b/lib/open_api_spex/plug/render_spec.ex @@ -25,7 +25,7 @@ defmodule OpenApiSpex.Plug.RenderSpec do @behaviour Plug - @json_encoder Enum.find([Jason, Poison], &Code.ensure_loaded?/1) + @json_encoder Enum.find([JSON, Jason, Poison], &Code.ensure_loaded?/1) @impl Plug def init(opts), do: opts From 75295c61a1d49dfe5355713e0603cafc597b6256 Mon Sep 17 00:00:00 2001 From: Diederick Lawson Date: Mon, 10 Mar 2025 17:58:05 +0100 Subject: [PATCH 2/2] Throws error when only the JSON library is available and the user set the `pretty` option to `true` --- lib/mix/tasks/openapi.spec.json.ex | 24 ++++++++++++++++-------- lib/open_api_spex/controller_specs.ex | 4 +++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/mix/tasks/openapi.spec.json.ex b/lib/mix/tasks/openapi.spec.json.ex index ebb1244c..47cebaea 100644 --- a/lib/mix/tasks/openapi.spec.json.ex +++ b/lib/mix/tasks/openapi.spec.json.ex @@ -36,20 +36,16 @@ defmodule Mix.Tasks.Openapi.Spec.Json do {opts, _, _} = OptionParser.parse(argv, strict: [start_app: :boolean]) Keyword.get(opts, :start_app, true) |> maybe_start_app() - OpenApiSpex.ExportSpec.call(argv, &encode/2, @default_filename) + OpenApiSpex.ExportSpec.call(argv, &export!/2, @default_filename) end defp maybe_start_app(true), do: Mix.Task.run("app.start") defp maybe_start_app(_), do: Mix.Task.run("app.config", preload_modules: true) - # Unfortunately, the built-in JSON module in Elixir 1.18.x doesn't support the `pretty` option - # So we need to take Jason or Poison to be able to support this feature - defp json_encoder(), do: Enum.find([Jason, Poison], &Code.ensure_loaded?/1) + defp export!(spec, %{pretty: pretty}) do + encoder = json_encoder() - defp encode(spec, %{pretty: pretty}) do - spec - |> json_encoder().encode(pretty: pretty) - |> case do + case encode(encoder, spec, pretty) do {:ok, json} -> "#{json}\n" @@ -57,4 +53,16 @@ defmodule Mix.Tasks.Openapi.Spec.Json do Mix.raise("could not encode #{inspect(spec)}, error: #{inspect(error)}.") end end + + defp json_encoder(), do: Enum.find([Jason, Poison, JSON], &Code.ensure_loaded?/1) + + # Unfortunately, the built-in JSON module in Elixir 1.18.x doesn't support the `pretty` option + # So we need to take Jason or Poison to be able to support this feature + + defp encode(JSON, _spec, true), + do: {:error, "the default JSON encoder does not support the pretty option"} + + defp encode(JSON, spec, _pretty), do: {:ok, JSON.encode!(spec)} + + defp encode(encoder, spec, pretty), do: encoder.encode(spec, pretty: pretty) end 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{