Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Confusion around reading YAML into ApiSpec #582

Open
mengelseatz opened this issue Dec 10, 2023 · 5 comments
Open

Confusion around reading YAML into ApiSpec #582

mengelseatz opened this issue Dec 10, 2023 · 5 comments

Comments

@mengelseatz
Copy link

The documentation implies that you can use either Elixir code to create your OpenAPI schema or parse and read a YAML( or JSON) OpenAPI schema. I need to do the former and I have this in my code and it parses and reads the file without an error. I also have configured the SwaggerUI and it renders the API documentation correctly which to me implies this is configured correctly:

  defmodule MyApiWeb.ApiSpec do
    @moduledoc false
  
    alias OpenApiSpex.{Components, Info, OpenApi, Paths, Server}
    alias MyApiWeb.{Endpoint, Router}
    @behaviour OpenApi
  
    @impl OpenApi
    def spec do
      open_api_spec_from_yaml =
        "openapi/v1/car.yaml"
        |> YamlElixir.read_all_from_file!()
        |> List.first()
        |> OpenApiSpex.OpenApi.Decode.decode()
  
      # IO.inspect(open_api_spec_from_yaml)
      # Discover request/response schemas from path specs
      open_api_spec_from_yaml |> OpenApiSpex.resolve_schema_modules()
    end
  end

But when I'm trying to use the functionality described in the documentation it doesn't appear that the data structures are created correctly in the modules used by the documentation. e.g. the README section "Validate Responses" has a test like this which I modified to use my module

    use ExUnit.Case
    import OpenApiSpex.TestAssertions
    
    test "MyOffersRequest example matches schema" do
      api_spec = MyApiWeb.ApiSpec.spec()
      schema = MyApiWeb.Schemas. MyOffersRequest.schema()
      assert_schema(schema.example, "MyOffersRequest, api_spec)
    end

And if I run the test I get the following error

 ** (UndefinedFunctionError) function MyApiWeb.Schemas.MyOffersRequest.schema/0 is undefined (module MyApiWeb.Schemas.MyOffersRequest is not available)
 code: schema = MyApiWeb.Schemas.MyOffersRequest.schema()
 stacktrace:
   MyApiWeb.Schemas.MyOffersRequest.schema()

So in effect MyApiWeb.Schemas is empty and I saw this while trying to do request validation in my Phoenix controllers as well. I know there is a PutApiSpec that's mentioned in doing validation which I have setup in my router.ex (see comment above about the swagger ui working correctly which I believe relies on this) but I get the same error there as if the module created doesn't get populated with the data from my OpenAPI schema YAML file even though it parses it and can render the docs. Note I did a quick test with the Ruby on Rails committee gem to confirm if this was something wrong with my schema that maybe OpenApiSpex didn't like and it worked fine without any issues.

If someone could steer me in the right direction I'd appreciate it.

@mengelseatz
Copy link
Author

mengelseatz commented Dec 10, 2023

So as to not confuse this with my specific API I moved to a new phoenix application and am using the OpenAPI 3 petstore.yaml and still running into similar issues. The documentation doesn't describe this use case where the operation that is used in the controller comes from an already parsed API schema. I'm trying things like this but it's not working
operation(:create, PetstoreApiWeb.ApiSpec.spec().paths()["/pets"].post)

@pauldemarco
Copy link

Interesting. I am considering using this for an already defined openapi.yaml file. The documents do seem to suggest you can read them in and get all the benefits of validation.

This was a few months ago, any updates?

@zorbash
Copy link
Contributor

zorbash commented Jul 26, 2024

@pauldemarco My understanding by reviewing the code and the README is that for YAML and JSON specs, one needs to implement their own wrapper for validations. You can try using a plug like the following:

defmodule MyServiceWeb.ValidateOpenAPI do
  @moduledoc """
  ## Usage

      pipeline :api_docs do
        plug OpenApiSpex.Plug.PutApiSpec, module: MyServiceWeb.ApiSpec
      end

      scope "/v1", OrdersServiceWeb.V2, as: :v1 do
        pipe_through :api_docs

        post "/my_resouce", MyResourceController, :create
      end

      defmodule MyResourceController do
        use MyServiceWeb, :controller

        plug(MyServiceWeb.ValidateWithOpenAPI, [operation_id: "my-resource-create-v1"] when action in [:create])

        def create(conn, params) do
        end
      end
  """

  @behaviour Plug

  require Logger

  alias OpenApiSpex.Operation

  @impl Plug
  def init(opts), do: Map.new(opts)

  @impl Plug
  def call(
        %{private: %{open_api_spex: _}} = conn,
        %{operation_id: operation_id}
      ) do
    {spec, operation_lookup} = OpenApiSpex.Plug.PutApiSpec.get_spec_and_operation_lookup(conn)

    case operation_lookup[operation_id] do
      nil ->
        warn(%{
          conn: conn,
          operation_id: operation_id,
          errors: "Misconfigured OpenAPI validation for #{inspect(operation_id)}"
        })

        conn

      operation ->
        validate_operation(conn, operation, spec)
    end
  end

  def call(conn, _opts), do: conn

  defp validate_operation(conn, %Operation{operationId: operation_id} = operation, spec) do
    case OpenApiSpex.cast_and_validate(spec, operation, conn, nil, []) do
      {:ok, _conn} ->
        conn

      {:error, errors} ->
        render_errors(errors)
    end
  end

  defp render_errors(conn, %{errors: errors}) do
    conn
    |> Conn.put_resp_content_type("application/json")
    |> Conn.send_resp(:unprocessable_entity, %{errors: format_errors(errors)})
  end

  defp format_errors(errors) do
    Enum.map(errors, fn error ->
      pointer = OpenApiSpex.path_to_string(error)

      %{
        title: "Invalid value",
        source: %{
          pointer: pointer
        },
        detail: to_string(error)
      }
    end)
  end
end

@pauldemarco
Copy link

This was it! I am now seeing if there is a better way to connect all of the operationId's with Elixir functions in the module.
As of now, I just have to make separate plug lines for each like so:

        plug(MyServiceWeb.ValidateWithOpenAPI, [operation_id: "my-resource-create-v1"] when action in [:create])
        plug(MyServiceWeb.ValidateWithOpenAPI, [operation_id: "my-resource-update-v1"] when action in [:update])
        plug(MyServiceWeb.ValidateWithOpenAPI, [operation_id: "my-resource-delete-v1"] when action in [:delete])

Any thoughts on how to consolidate?

@zorbash
Copy link
Contributor

zorbash commented Aug 16, 2024

This was it! I am now seeing if there is a better way to connect all of the operationId's with Elixir functions in the module. As of now, I just have to make separate plug lines for each like so:

        plug(MyServiceWeb.ValidateWithOpenAPI, [operation_id: "my-resource-create-v1"] when action in [:create])
        plug(MyServiceWeb.ValidateWithOpenAPI, [operation_id: "my-resource-update-v1"] when action in [:update])
        plug(MyServiceWeb.ValidateWithOpenAPI, [operation_id: "my-resource-delete-v1"] when action in [:delete])

Any thoughts on how to consolidate?

You can tweak MyServiceWeb.ValidateWithOpenAPI to accept a map of operation_ids and actions. For example:

plug(MyServiceWeb.ValidateWithOpenAPI, [
  %{operation_id: "my-resource-delete-v1", action: :delete}, 
  %{operation_id: "my-resource-update"], action: :update}
] when action in [:delete, :update])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants