open-api-spex / open_api_spex

Open API Specifications for Elixir Plug applications
Mozilla Public License 2.0
701 stars 183 forks source link

cast_and_validate purges file upload struct from controller parameters #370

Open superruzafa opened 3 years ago

superruzafa commented 3 years ago

Hi.

This is more a question than an issue.

In the Phoenix framework, when doing a file upload I used to fetch the %Plug.Upload structure from the 2nd parameter passed to the controller's action:

def action(conn, %{"file_upload" => %Plug.Upload{}}) do
...
end

I created a spec in order to allow file uploads as noted in similar issues:

OpenApiSpex.schema(%{
  title: "FileUpload",
  type: :object,
  properties: %{
    file_upload: %Schema{
      type: :string,
      format: :binary
    }
  }
})

After calling OpenApiSpex.cast_and_validate/4 I noticed that the upload is deleted from the 2nd parameter passed to the controller.

I also check that the upload can be accessed from the conn's body_params field, but my question is: is there any way to preserve it in the controller's 2nd parameter?

Thanks.

lucacorti commented 3 years ago

@superruzafa Same here, looks like the root cause is the same as #92. OpenApiSpex is changing the phoenix params and this causes issues with anything expecting them to be there.

mbuhot commented 3 years ago

Looks like when we CastAndValidate a schema with type: :string, format: :binary it should allow a %Plug.Upload{} struct to be passed through.

https://swagger.io/docs/specification/describing-request-body/file-upload/

petermueller commented 1 year ago

Hey, coming across this a year+ later. Is it possible that the OpenApiSpex.Cast.OneOf is not handling it correctly. I'm pretty sure I have my schema correct, but when I don't use a oneOf, it works, but otherwise I get an "Enum. protocol not implemented for Plug.Upload" error.

image: %Schema{
  oneOf: [%Schema{type: :string, format: :binary}, Base64ObjectImageUpload]
}

I am also using a MediaType with a custom override encoding:

content: %{
  "multipart/form-data" => %OpenApiSpex.MediaType{
    schema: __MODULE__,
    encoding: %{"image" => %OpenApiSpex.Encoding{contentType: "image/png"}}
  },
  "application/json" => %OpenApiSpex.MediaType{schema: __MODULE__}
},

Does this seem likely, or more likely that I just messed up my oneOf 😅

lucacorti commented 1 year ago

@petermueller can you test #455 and see if it solves your issue?

petermueller commented 1 year ago

@lucacorti, can do 👍🏻 I'll report back in the morning

petermueller commented 1 year ago

Unfortunately no. Same error :-/

(EDIT: This was after I blew away the deps and _build just in case)

Stack

[error] #PID<0.4803.0> running MyApp.Endpoint (connection #PID<0.4802.0>, stream id 1) terminated
Server: local.myapp.com:80 (http)
Request: PUT /api/v2/my_resource/55/
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Enumerable not implemented for %Plug.Upload{content_type: "image/png", filename: "C84A8E27-EEE4-4439-8007-4AC937E9AD93.png", path: "/var/folders/wh/s4xndcwj56vdh19q0cd9628w0000gn/T/plug-1664/multipart-1664895328-565347161995327-1"} of type Plug.Upload (a struct)
        (elixir 1.13.4) lib/enum.ex:1: Enumerable.impl_for!/1
        (elixir 1.13.4) lib/enum.ex:143: Enumerable.reduce/3
        (elixir 1.13.4) lib/enum.ex:4144: Enum.reduce/3
        (open_api_spex 3.13.0) lib/open_api_spex/cast/object.ex:150: OpenApiSpex.Cast.Object.get_additional_properties/2
        (open_api_spex 3.13.0) lib/open_api_spex/cast/object.ex:132: OpenApiSpex.Cast.Object.cast_additional_properties/2
        (open_api_spex 3.13.0) lib/open_api_spex/cast/object.ex:24: OpenApiSpex.Cast.Object.cast/1
        (open_api_spex 3.13.0) lib/open_api_spex/cast/one_of.ex:17: anonymous fn/2 in OpenApiSpex.Cast.OneOf.cast/1
        (elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
        (open_api_spex 3.13.0) lib/open_api_spex/cast/one_of.ex:13: OpenApiSpex.Cast.OneOf.cast/1
        (open_api_spex 3.13.0) lib/open_api_spex/cast/object.ex:170: OpenApiSpex.Cast.Object.cast_property/2
        (open_api_spex 3.13.0) lib/open_api_spex/cast/object.ex:113: anonymous fn/4 in OpenApiSpex.Cast.Object.cast_properties/1
        (stdlib 4.0.1) maps.erl:411: :maps.fold_1/3
        (open_api_spex 3.13.0) lib/open_api_spex/cast/object.ex:112: OpenApiSpex.Cast.Object.cast_properties/1
        (open_api_spex 3.13.0) lib/open_api_spex/cast/object.ex:28: OpenApiSpex.Cast.Object.cast/1
        (open_api_spex 3.13.0) lib/open_api_spex/operation2.ex:37: OpenApiSpex.Operation2.cast/5
        (open_api_spex 3.13.0) lib/open_api_spex/plug/cast_and_validate.ex:82: OpenApiSpex.Plug.CastAndValidate.call/2
        (myapp 0.1.0) lib/my_app/controllers/my_resource_controller.ex:1: MyApp.MyResourceController.phoenix_controller_pipeline/2
        (phoenix 1.6.11) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
        (myapp 0.1.0) lib/my_app/endpoint.ex:1: MyApp.Endpoint.plug_builder_call/2
        (myapp 0.1.0) lib/my_app/endpoint.ex:1: MyApp.Endpoint."call (overridable 3)"/2

Operation

  operation :update,
    summary: "My Description",
    parameters: [MyApp.Schemas.PathParameters.id_param(:id, "My Resource ID")],
    request_body: UpdateMyResourceRequest.request_body()

  def update(conn, %{id: id}) do
   # ...

Schema and custom request_body function

defmodule MyApp.Schemas.UpdateMyResourceRequest. do
  @moduledoc """
  The `UpdateMyResourceRequest` `OpenApiSpex` Schema
  """

  alias OpenApiSpex.Schema

  require OpenApiSpex

  OpenApiSpex.schema(%{
    description: "Updates my resources",
    type: :object,
    properties: %{
      thing_a: %Schema{
        type: :string,
        description: "thing_a"
      },
      thing_b: %Schema{
        type: :string,
        description: "thing_b"
      },
      thing_c_dt: %Schema{
        type: :string,
        description: "thing_c_dt",
        format: :"date-time",
        nullable: false
      },
      thing_d_enum: %Schema{
        type: :string,
        enum: [:asdf, :qwer]
      },
      # image: %Schema{type: :string, format: :binary}
      image: %Schema{
        oneOf: [%Schema{type: :string, format: :binary}, Base64ObjectImageUpload]
      }
    },
    example: %{
      "thing_a" => "asdf",
      "thing_c_dt" => "2021-01-30 15:00:00-04:00",
      "image" => "thisistheimagebinary"
    }
  })

  def request_body do
    %OpenApiSpex.RequestBody{
      description: "Update My Resource",
      content: %{
        "multipart/form-data" => %OpenApiSpex.MediaType{
          schema: __MODULE__,
          encoding: %{"image" => %OpenApiSpex.Encoding{contentType: "image/png"}}
        },
        "application/json" => %OpenApiSpex.MediaType{schema: __MODULE__}
      },
      required: true
    }
  end
end