diff --git a/lib/open_api_spex/cast_parameters.ex b/lib/open_api_spex/cast_parameters.ex index dc6f6138..d08b65a0 100644 --- a/lib/open_api_spex/cast_parameters.ex +++ b/lib/open_api_spex/cast_parameters.ex @@ -75,6 +75,9 @@ defmodule OpenApiSpex.CastParameters do end defp cast_location(location, schema, components, conn) do + IO.inspect(conn, label: "conn") + IO.inspect({location, schema}, label: "casting {location, schema}") + params = get_params_by_location( conn, @@ -82,6 +85,8 @@ defmodule OpenApiSpex.CastParameters do schema.properties |> Map.keys() |> Enum.map(&Atom.to_string/1) ) + IO.inspect(params, label: "params") + ctx = %Cast{ value: params, schema: schema, diff --git a/lib/open_api_spex/json_api_helpers.ex b/lib/open_api_spex/json_api_helpers.ex new file mode 100644 index 00000000..3ea34aab --- /dev/null +++ b/lib/open_api_spex/json_api_helpers.ex @@ -0,0 +1,39 @@ +defmodule OpenApiSpex.JsonApiHelpers do + alias OpenApiSpex.JsonApiHelpers.{JsonApiDocument, JsonApiResource} + + def document_schema(document) do + JsonApiDocument.schema(document) + end + + def resource_schema(resource) do + JsonApiResource.schema(resource) + end + + defmacro generate_document_schema(attrs) do + quote do + require OpenApiSpex + + @document struct!(JsonApiDocument, unquote(attrs)) + def document, do: @document + + @document_schema JsonApiDocument.schema(@document) + def document_schema, do: @document_schema + + OpenApiSpex.schema(@document_schema) + end + end + + defmacro generate_resource_schema(attrs) do + quote do + require OpenApiSpex + + @resource struct!(JsonApiResource, unquote(attrs)) + def resource, do: @resource + + @resource_schema JsonApiResource.schema(@resource) + def resource_schema, do: @resource_schema + + OpenApiSpex.schema(@resource_schema) + end + end +end diff --git a/lib/open_api_spex/json_api_helpers/json_api_document.ex b/lib/open_api_spex/json_api_helpers/json_api_document.ex new file mode 100644 index 00000000..4c3c184c --- /dev/null +++ b/lib/open_api_spex/json_api_helpers/json_api_document.ex @@ -0,0 +1,51 @@ +defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do + alias OpenApiSpex.Schema + alias OpenApiSpex.JsonApiHelpers.JsonApiResource + + defstruct resource: nil, + multiple: false, + title: nil, + "x-struct": nil + + def schema(%__MODULE__{} = document) do + if not is_binary(document.title) do + raise "%JsonApiDocument{} :title is required and must be a string" + end + + resource = document.resource + resource_item_schema = JsonApiResource.schema(resource) + + resource_title = + case resource_item_schema do + %Schema{} = schema -> schema.title + module when is_atom(module) and not is_nil(module) -> module.schema().title + end + + resource_schema = + if document.multiple do + %Schema{ + type: :array, + items: resource_item_schema, + title: resource_title <> "List" + } + else + resource_item_schema + end + + %Schema{ + type: :object, + properties: %{ + data: resource_schema + }, + required: [:data], + title: document.title, + "x-struct": document."x-struct" + } + end + + def schema(document_attrs) when is_list(document_attrs) or is_map(document_attrs) do + __MODULE__ + |> struct!(document_attrs) + |> schema() + end +end diff --git a/lib/open_api_spex/json_api_helpers/json_api_resource.ex b/lib/open_api_spex/json_api_helpers/json_api_resource.ex new file mode 100644 index 00000000..4e572496 --- /dev/null +++ b/lib/open_api_spex/json_api_helpers/json_api_resource.ex @@ -0,0 +1,41 @@ +defmodule OpenApiSpex.JsonApiHelpers.JsonApiResource do + alias OpenApiSpex.Schema + + defstruct additionalProperties: nil, + properties: %{}, + required: [], + title: nil + + def schema(resource) when is_atom(resource) and not is_nil(resource) do + resource.schema()."x-struct" + end + + def schema(%__MODULE__{} = resource) do + if not is_binary(resource.title) do + raise "%JsonApiResource{} :title is required and must be a string" + end + + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + type: %Schema{type: :string}, + attributes: attributes_schema(resource) + }, + required: [:id, :type], + title: resource.title <> "Resource" + } + end + + def attributes_schema(%__MODULE__{} = resource) do + if not is_binary(resource.title) do + raise "%JsonApiResource{} :title is required and must be a string" + end + + %Schema{ + type: :object, + properties: resource.properties, + title: resource.title <> "Attributes" + } + end +end diff --git a/lib/open_api_spex/schema_resolver.ex b/lib/open_api_spex/schema_resolver.ex index a948b15b..62ccc3a7 100644 --- a/lib/open_api_spex/schema_resolver.ex +++ b/lib/open_api_spex/schema_resolver.ex @@ -37,7 +37,7 @@ defmodule OpenApiSpex.SchemaResolver do {paths, schemas} = resolve_schema_modules_from_paths(spec.paths, schemas) schemas = resolve_schema_modules_from_schemas(schemas) {responses, _} = resolve_schema_modules_from_responses(responses, schemas) - %{spec | paths: paths, components: %{components| schemas: schemas, responses: responses}} + %{spec | paths: paths, components: %{components | schemas: schemas, responses: responses}} end defp resolve_schema_modules_from_paths(paths = %{}, schemas = %{}) do @@ -217,6 +217,17 @@ defmodule OpenApiSpex.SchemaResolver do properties: properties } + IO.inspect(schema, label: "schema") + + # title = schema.title + + # schemas = + # case {title, schemas} do + # {nil, _} -> schemas + # {_, %{^title => _}} -> schemas + # _ -> Map.put(schemas, title, schema) + # end + {schema, schemas} end diff --git a/test/cast_parameters_test.exs b/test/cast_parameters_test.exs index 54a98e53..dff61c62 100644 --- a/test/cast_parameters_test.exs +++ b/test/cast_parameters_test.exs @@ -35,26 +35,39 @@ defmodule OpenApiSpex.CastParametersTest do {:ok, conn} = CastParameters.cast(conn, operation, components) assert %{params: %{includeInactive: false}} = conn end + + # test "params are validated" do + # conn = create_conn(query_string: "/invalid") + # operation = create_operation() + # components = create_empty_components() + # {:error, error} = CastParameters.cast(conn, operation, components) + # assert error == :error + # end + + test "path params are validated" do + end end - defp create_conn() do + defp create_conn(opts \\ []) do + opts = Map.new(opts) + query_string = opts[:query_string] + :get - |> Plug.Test.conn("/api/users/") + |> Plug.Test.conn("/api/users#{query_string}") |> Plug.Conn.put_req_header("content-type", "application/json") |> Plug.Conn.fetch_query_params() end defp create_conn_with_unexpected_path_param() do - :get - |> Plug.Test.conn("/api/users?invalid_key=value") - |> Plug.Conn.put_req_header("content-type", "application/json") - |> Plug.Conn.fetch_query_params() + create_conn(query_string: "?invalid_key=value") end defp create_operation() do %Operation{ parameters: [ - Operation.parameter(:id, :query, :string, "User ID", example: "1"), + Operation.parameter(:id, :path, %Schema{type: :string, pattern: ~r/^\d+$/}, "User ID", + example: "1" + ), Operation.parameter( :includeInactive, :query, diff --git a/test/controller_test.exs b/test/controller_test.exs index c4d125b1..fbbb3a14 100644 --- a/test/controller_test.exs +++ b/test/controller_test.exs @@ -65,14 +65,14 @@ defmodule OpenApiSpex.ControllerTest do test "has no docs when false" do assert capture_io(:stderr, fn -> - refute @controller.open_api_operation(:skip_this_doc) - end) == "" + refute @controller.open_api_operation(:skip_this_doc) + end) == "" end test "prints warn when no @doc specified" do assert capture_io(:stderr, fn -> - refute @controller.open_api_operation(:no_doc_specified) - end) =~ ~r/warning:/ + refute @controller.open_api_operation(:no_doc_specified) + end) =~ ~r/warning:/ end end @@ -96,7 +96,7 @@ defmodule OpenApiSpex.ControllerTest do end test "fails on both type and schema specified" do - assert_raise ArgumentError, ~r/Both :type and :schema options were specified/, fn -> + assert_raise ArgumentError, ~r/Both :type and :schema options were specified/, fn -> @controller.open_api_operation(:non_exlusive_paramter_type_schema_docs) end end diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs new file mode 100644 index 00000000..1821e48d --- /dev/null +++ b/test/json_api_helpers_test.exs @@ -0,0 +1,82 @@ +defmodule OpenApiSpex.JsonApiHelpersTest do + use ExUnit.Case, async: true + + alias OpenApiSpexTest.{CartDocument, CartIndexDocument, CartResource} + alias OpenApiSpex.{JsonApiHelpers, Schema} + alias OpenApiSpex.JsonApiHelpers.JsonApiResource + + describe "from operation specs" do + test "index action" do + spec = OpenApiSpexTest.ApiSpec2.spec() + assert %Schema{} = _schema = spec.components.schemas["CartIndexResponse"] + end + end + + describe "generate_resource_document/1" do + test "generate schema/0" do + assert %Schema{} = schema = CartDocument.schema() + assert schema.title == "CartDocument" + assert %{data: _} = schema.properties + assert schema.properties.data.title == CartResource.schema().title + end + + test "generate schema for index document" do + assert %Schema{} = schema = CartIndexDocument.schema() + assert schema.title == "CartIndexDocument" + assert %{data: _} = schema.properties + assert schema.properties.data.type == :array + assert schema.properties.data.items == OpenApiSpexTest.CartResource + end + end + + describe "generate_resource_schema/1" do + test "generate resource/0 and resource_schema/0" do + assert %JsonApiResource{} = CartResource.resource() + assert %Schema{} = schema = CartResource.schema() + assert schema.title == "CartResource" + end + end + + describe "resource_schema/1" do + test "attributes" do + resource = CartResource.resource() + schema = JsonApiHelpers.resource_schema(resource) + assert schema.properties.attributes == JsonApiResource.attributes_schema(resource) + assert %Schema{} = schema.properties.id + assert %Schema{} = schema.properties.type + end + + test "title" do + resource = CartResource.resource() + schema = JsonApiHelpers.resource_schema(resource) + assert schema.title == "CartResource" + end + end + + describe "attributes_schema/1" do + test "generates schema with same properties" do + resource = CartResource.resource() + schema = JsonApiResource.attributes_schema(resource) + assert schema.properties == resource.properties + end + + test "generates title" do + resource = CartResource.resource() + schema = JsonApiResource.attributes_schema(resource) + assert schema.title == "CartAttributes" + end + + test ":title must be a string" do + resource = CartResource.resource() + resource = %{resource | title: nil} + + assert_raise( + RuntimeError, + "%JsonApiResource{} :title is required and must be a string", + fn -> + JsonApiResource.attributes_schema(resource) + end + ) + end + end +end diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex index ec9ef9c5..1f2ecc98 100644 --- a/test/support/api_spec.ex +++ b/test/support/api_spec.ex @@ -24,7 +24,7 @@ defmodule OpenApiSpexTest.ApiSpec do }, components: %Components{ schemas: - for schemaMod <- [ + for schema_mod <- [ Schemas.Pet, Schemas.PetType, Schemas.Cat, @@ -35,7 +35,7 @@ defmodule OpenApiSpexTest.ApiSpec do Schemas.Primitive ], into: %{} do - schema = schemaMod.schema() + schema = schema_mod.schema() {schema.title, schema} end }, diff --git a/test/support/api_spec_2.ex b/test/support/api_spec_2.ex new file mode 100644 index 00000000..4fcfecdb --- /dev/null +++ b/test/support/api_spec_2.ex @@ -0,0 +1,30 @@ +defmodule OpenApiSpexTest.ApiSpec2 do + alias OpenApiSpex.{OpenApi, Contact, License, Paths, Server, Info} + alias OpenApiSpexTest.Router2 + + @behaviour OpenApi + + @impl OpenApi + def spec() do + %OpenApi{ + servers: [ + %Server{url: "http://example.com"} + ], + info: %Info{ + title: "A", + version: "3.0", + contact: %Contact{ + name: "joe", + email: "Joe@gmail.com", + url: "https://help.joe.com" + }, + license: %License{ + name: "MIT", + url: "http://mit.edu/license" + } + }, + paths: Paths.from_router(Router2) + } + |> OpenApiSpex.resolve_schema_modules() + end +end diff --git a/test/support/cart_document.ex b/test/support/cart_document.ex new file mode 100644 index 00000000..9a1d2af7 --- /dev/null +++ b/test/support/cart_document.ex @@ -0,0 +1,11 @@ +defmodule OpenApiSpexTest.CartDocument do + alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpexTest.CartResource + + require OpenApiSpex.JsonApiHelpers + + JsonApiHelpers.generate_document_schema( + title: "CartDocument", + resource: CartResource.resource() + ) +end diff --git a/test/support/cart_index_document.ex b/test/support/cart_index_document.ex new file mode 100644 index 00000000..2c7491a0 --- /dev/null +++ b/test/support/cart_index_document.ex @@ -0,0 +1,12 @@ +defmodule OpenApiSpexTest.CartIndexDocument do + alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpexTest.CartResource + + require OpenApiSpex.JsonApiHelpers + + JsonApiHelpers.generate_document_schema( + title: "CartIndexDocument", + multiple: true, + resource: CartResource + ) +end diff --git a/test/support/cart_resource.ex b/test/support/cart_resource.ex new file mode 100644 index 00000000..c8818bd0 --- /dev/null +++ b/test/support/cart_resource.ex @@ -0,0 +1,14 @@ +defmodule OpenApiSpexTest.CartResource do + alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpex.Schema + + require OpenApiSpex.JsonApiHelpers + + JsonApiHelpers.generate_resource_schema( + title: "Cart", + properties: %{ + total: %Schema{type: :integer} + }, + additionalProperties: false + ) +end diff --git a/test/support/endpoint.ex b/test/support/endpoint.ex index 7ee48fc1..73cc4500 100644 --- a/test/support/endpoint.ex +++ b/test/support/endpoint.ex @@ -1,4 +1,5 @@ defmodule OpenApiSpexTest.Endpoint do use Phoenix.Endpoint, otp_app: :open_api_spex_test -end + plug OpenApiSpexTest.Router +end diff --git a/test/support/json_api_controller.ex b/test/support/json_api_controller.ex new file mode 100644 index 00000000..c5937eb7 --- /dev/null +++ b/test/support/json_api_controller.ex @@ -0,0 +1,45 @@ +defmodule OpenApiSpexTest.JsonApiController do + use Phoenix.Controller + use OpenApiSpex.Controller + + alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpexTest.CartResource + + require OpenApiSpex.JsonApiHelpers + + @doc """ + Get a list of carts. + """ + @doc responses: [ + ok: { + "Carts", + "application/json", + JsonApiHelpers.document_schema( + title: "CartIndexResponse", + resource: CartResource, + multiple: true, + "x-struct": "CartIndexResponse" + ) + } + ] + def index(conn, _params) do + json(conn, %{data: []}) + end + + @doc """ + Get a cart by ID. + """ + @doc responses: [ + ok: { + "Cart", + "application/json", + JsonApiHelpers.document_schema( + title: "Cart", + resource: CartResource.resource() + ) + } + ] + def show(conn, _params) do + json(conn, %{data: %{}}) + end +end diff --git a/test/support/phoenix_controller_test.exs b/test/support/phoenix_controller_test.exs new file mode 100644 index 00000000..bcb2d31c --- /dev/null +++ b/test/support/phoenix_controller_test.exs @@ -0,0 +1,24 @@ +defmodule OpenApiSpex.PhoenixControllerTest do + use ExUnit.Case, async: true + + use Phoenix.ConnTest + import OpenApiSpexTest.Router.Helpers + + # The default endpoint for testing + @endpoint OpenApiSpexTest.Endpoint + + test "foo" do + resp_body = + conn_with_headers() + |> post("/api/pets/nope/adopt") + |> json_response(200) + + assert resp_body == :foo + end + + defp conn_with_headers do + build_conn() + |> Plug.Conn.put_req_header("accept", "application/json") + |> Plug.Conn.put_req_header("content-type", "application/json") + end +end diff --git a/test/support/router.ex b/test/support/router.ex index f33ebbd8..e436a66f 100644 --- a/test/support/router.ex +++ b/test/support/router.ex @@ -27,5 +27,8 @@ defmodule OpenApiSpexTest.Router do get "/utility/echo/any", UtilityController, :echo_any post "/utility/echo/body_params", UtilityController, :echo_body_params + + get "/jsonapi/carts", JsonApiController, :index + get "/jsonapi/carts/:id", JsonApiController, :show end end diff --git a/test/support/router_2.ex b/test/support/router_2.ex new file mode 100644 index 00000000..35efbbce --- /dev/null +++ b/test/support/router_2.ex @@ -0,0 +1,17 @@ +defmodule OpenApiSpexTest.Router2 do + use Phoenix.Router + alias Plug.Parsers + alias OpenApiSpex.Plug.PutApiSpec + + pipeline :api do + plug :accepts, ["json"] + plug PutApiSpec, module: OpenApiSpexTest.ApiSpec + plug Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Jason + end + + scope "/api", OpenApiSpexTest do + pipe_through :api + get "/jsonapi/carts", JsonApiController, :index + # get "/jsonapi/carts/:id", JsonApiController, :show + end +end