From 907920377d7d76a7b6bd5789c644e4a9bf222586 Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Mon, 15 Jun 2020 13:08:09 -0700 Subject: [PATCH 01/10] Basics of `%JsonApiResource{}` `resource_schema/1` and `resource_attributes/1` --- lib/open_api_spex/json_api_helpers.ex | 31 ++++++++++++ .../json_api_helpers/json_api_resource.ex | 6 +++ test/json_api_helpers_test.exs | 49 +++++++++++++++++++ test/support/cart_resource.ex | 14 ++++++ 4 files changed, 100 insertions(+) create mode 100644 lib/open_api_spex/json_api_helpers.ex create mode 100644 lib/open_api_spex/json_api_helpers/json_api_resource.ex create mode 100644 test/json_api_helpers_test.exs create mode 100644 test/support/cart_resource.ex 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..bebf7bff --- /dev/null +++ b/lib/open_api_spex/json_api_helpers.ex @@ -0,0 +1,31 @@ +defmodule OpenApiSpex.JsonApiHelpers do + alias OpenApiSpex.JsonApiHelpers.JsonApiResource + alias OpenApiSpex.Schema + + def resource_schema(%JsonApiResource{} = resource) do + if not is_binary(resource.title) do + raise "%JsonApiResource{} :title is required and must be a string" + end + + %Schema{ + properties: %{ + id: %Schema{type: :string}, + type: %Schema{type: :string}, + attributes: attributes_schema(resource) + }, + required: [:id, :type], + title: resource.title <> "Resource" + } + end + + def attributes_schema(%JsonApiResource{} = resource) do + if not is_binary(resource.title) do + raise "%JsonApiResource{} :title is required and must be a string" + end + + %Schema{ + properties: resource.properties, + title: resource.title <> "Attributes" + } + 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..9312f1c5 --- /dev/null +++ b/lib/open_api_spex/json_api_helpers/json_api_resource.ex @@ -0,0 +1,6 @@ +defmodule OpenApiSpex.JsonApiHelpers.JsonApiResource do + defstruct additionalProperties: nil, + properties: %{}, + required: [], + title: nil +end diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs new file mode 100644 index 00000000..8dcc650d --- /dev/null +++ b/test/json_api_helpers_test.exs @@ -0,0 +1,49 @@ +defmodule OpenApiSpex.JsonApiHelpersTest do + use ExUnit.Case, async: true + + alias OpenApiSpexTest.CartResource + alias OpenApiSpex.{JsonApiHelpers, Schema} + + describe "resource_schema/1" do + test "attributes" do + resource = CartResource.resource() + schema = JsonApiHelpers.resource_schema(resource) + assert schema.properties.attributes == JsonApiHelpers.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 = JsonApiHelpers.attributes_schema(resource) + assert schema.properties == resource.properties + end + + test "generates title" do + resource = CartResource.resource() + schema = JsonApiHelpers.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 -> + JsonApiHelpers.attributes_schema(resource) + end + ) + end + end +end diff --git a/test/support/cart_resource.ex b/test/support/cart_resource.ex new file mode 100644 index 00000000..92c0ecec --- /dev/null +++ b/test/support/cart_resource.ex @@ -0,0 +1,14 @@ +defmodule OpenApiSpexTest.CartResource do + alias OpenApiSpex.JsonApiHelpers.JsonApiResource + alias OpenApiSpex.Schema + + @resource %JsonApiResource{ + title: "Cart", + properties: %{ + total: %Schema{type: :integer} + }, + additionalProperties: false + } + + def resource, do: @resource +end From a2ed409fa587c7d9852e56c5611d044a7eebb392 Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Mon, 15 Jun 2020 13:39:52 -0700 Subject: [PATCH 02/10] Generate document schema --- lib/open_api_spex/json_api_helpers.ex | 55 +++++++++++++++++++++++++++ test/json_api_helpers_test.exs | 20 +++++++++- test/support/cart_document.ex | 14 +++++++ test/support/cart_resource.ex | 10 ++--- 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 test/support/cart_document.ex diff --git a/lib/open_api_spex/json_api_helpers.ex b/lib/open_api_spex/json_api_helpers.ex index bebf7bff..b9fe6f74 100644 --- a/lib/open_api_spex/json_api_helpers.ex +++ b/lib/open_api_spex/json_api_helpers.ex @@ -2,12 +2,28 @@ defmodule OpenApiSpex.JsonApiHelpers do alias OpenApiSpex.JsonApiHelpers.JsonApiResource alias OpenApiSpex.Schema + def document_schema(%JsonApiResource{} = resource) do + if not is_binary(resource.title) do + raise "%JsonApiResource{} :title is required and must be a string" + end + + %Schema{ + type: :object, + properties: %{ + data: resource_schema(resource) + }, + required: [:data], + title: resource.title <> "Document" + } + end + def resource_schema(%JsonApiResource{} = 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}, @@ -24,8 +40,47 @@ defmodule OpenApiSpex.JsonApiHelpers do end %Schema{ + type: :object, properties: resource.properties, title: resource.title <> "Attributes" } end + + defmacro generate_document_schema(attrs) do + quote do + require OpenApiSpex + + @resource struct!(OpenApiSpex.JsonApiHelpers.JsonApiResource, unquote(attrs)) + def resource, do: @resource + + @document_schema OpenApiSpex.JsonApiHelpers.document_schema(@resource) + def document_schema, do: @document_schema + + OpenApiSpex.schema(@document_schema) + end + end + + defmacro document(attrs) do + quote do + @resource struct!(OpenApiSpex.JsonApiHelpers.JsonApiResource, unquote(attrs)) + def resource, do: @resource + + @resource_schema OpenApiSpex.JsonApiHelpers.resource_schema(@resource) + def resource_schema, do: @resource_schema + end + end + + defmacro generate_resource_schema(attrs) do + quote do + require OpenApiSpex + + @resource struct!(OpenApiSpex.JsonApiHelpers.JsonApiResource, unquote(attrs)) + def resource, do: @resource + + @resource_schema OpenApiSpex.JsonApiHelpers.resource_schema(@resource) + def resource_schema, do: @resource_schema + + OpenApiSpex.schema(@resource_schema) + end + end end diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs index 8dcc650d..f6e1e107 100644 --- a/test/json_api_helpers_test.exs +++ b/test/json_api_helpers_test.exs @@ -1,8 +1,26 @@ defmodule OpenApiSpex.JsonApiHelpersTest do use ExUnit.Case, async: true - alias OpenApiSpexTest.CartResource + alias OpenApiSpexTest.{CartDocument, CartResource} alias OpenApiSpex.{JsonApiHelpers, Schema} + alias OpenApiSpex.JsonApiHelpers.JsonApiResource + + 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 + 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 diff --git a/test/support/cart_document.ex b/test/support/cart_document.ex new file mode 100644 index 00000000..9b8b50dc --- /dev/null +++ b/test/support/cart_document.ex @@ -0,0 +1,14 @@ +defmodule OpenApiSpexTest.CartDocument do + alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpex.Schema + + require OpenApiSpex.JsonApiHelpers + + JsonApiHelpers.generate_document_schema( + title: "Cart", + properties: %{ + total: %Schema{type: :integer} + }, + additionalProperties: false + ) +end diff --git a/test/support/cart_resource.ex b/test/support/cart_resource.ex index 92c0ecec..c8818bd0 100644 --- a/test/support/cart_resource.ex +++ b/test/support/cart_resource.ex @@ -1,14 +1,14 @@ defmodule OpenApiSpexTest.CartResource do - alias OpenApiSpex.JsonApiHelpers.JsonApiResource + alias OpenApiSpex.JsonApiHelpers alias OpenApiSpex.Schema - @resource %JsonApiResource{ + require OpenApiSpex.JsonApiHelpers + + JsonApiHelpers.generate_resource_schema( title: "Cart", properties: %{ total: %Schema{type: :integer} }, additionalProperties: false - } - - def resource, do: @resource + ) end From d387257a73720998c03b9b09408db598e3543649 Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Mon, 15 Jun 2020 13:55:32 -0700 Subject: [PATCH 03/10] Use `%JsonApiDocument{}` --- lib/open_api_spex/json_api_helpers.ex | 34 ++++++++++++++----- .../json_api_helpers/json_api_document.ex | 5 +++ test/support/cart_document.ex | 11 +++--- 3 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 lib/open_api_spex/json_api_helpers/json_api_document.ex diff --git a/lib/open_api_spex/json_api_helpers.ex b/lib/open_api_spex/json_api_helpers.ex index b9fe6f74..16ee14ce 100644 --- a/lib/open_api_spex/json_api_helpers.ex +++ b/lib/open_api_spex/json_api_helpers.ex @@ -1,19 +1,35 @@ defmodule OpenApiSpex.JsonApiHelpers do - alias OpenApiSpex.JsonApiHelpers.JsonApiResource + alias OpenApiSpex.JsonApiHelpers.{JsonApiDocument, JsonApiResource} alias OpenApiSpex.Schema - def document_schema(%JsonApiResource{} = resource) do - if not is_binary(resource.title) do - raise "%JsonApiResource{} :title is required and must be a string" + def document_schema(%JsonApiDocument{} = document) do + if not is_binary(document.title) do + raise "%JsonApiDocument{} :title is required and must be a string" end + resource = %{ + document.resource + | title: document.resource.title || document.title <> "Resource" + } + + resource_schema = %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + type: %Schema{type: :string}, + attributes: attributes_schema(resource) + }, + required: [:id, :type, :attributes], + title: resource.title + } + %Schema{ type: :object, properties: %{ - data: resource_schema(resource) + data: resource_schema }, required: [:data], - title: resource.title <> "Document" + title: document.title <> "Document" } end @@ -50,10 +66,10 @@ defmodule OpenApiSpex.JsonApiHelpers do quote do require OpenApiSpex - @resource struct!(OpenApiSpex.JsonApiHelpers.JsonApiResource, unquote(attrs)) - def resource, do: @resource + @document struct!(OpenApiSpex.JsonApiHelpers.JsonApiDocument, unquote(attrs)) + def document, do: @document - @document_schema OpenApiSpex.JsonApiHelpers.document_schema(@resource) + @document_schema OpenApiSpex.JsonApiHelpers.document_schema(@document) def document_schema, do: @document_schema OpenApiSpex.schema(@document_schema) 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..025ddff3 --- /dev/null +++ b/lib/open_api_spex/json_api_helpers/json_api_document.ex @@ -0,0 +1,5 @@ +defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do + defstruct resource: nil, + multiple: false, + title: nil +end diff --git a/test/support/cart_document.ex b/test/support/cart_document.ex index 9b8b50dc..25e43780 100644 --- a/test/support/cart_document.ex +++ b/test/support/cart_document.ex @@ -1,14 +1,17 @@ defmodule OpenApiSpexTest.CartDocument do alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpex.JsonApiHelpers.JsonApiResource alias OpenApiSpex.Schema require OpenApiSpex.JsonApiHelpers JsonApiHelpers.generate_document_schema( title: "Cart", - properties: %{ - total: %Schema{type: :integer} - }, - additionalProperties: false + resource: %JsonApiResource{ + properties: %{ + total: %Schema{type: :integer} + }, + additionalProperties: false + } ) end From 343e5979010a39077508c50d0630069121f64fab Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Mon, 15 Jun 2020 14:19:34 -0700 Subject: [PATCH 04/10] Represent document as a single resource or list of resources --- lib/open_api_spex/json_api_helpers.ex | 32 +++++++-------------------- test/json_api_helpers_test.exs | 10 ++++++++- test/support/cart_document.ex | 10 ++------- test/support/cart_index_document.ex | 12 ++++++++++ 4 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 test/support/cart_index_document.ex diff --git a/lib/open_api_spex/json_api_helpers.ex b/lib/open_api_spex/json_api_helpers.ex index 16ee14ce..911bba8b 100644 --- a/lib/open_api_spex/json_api_helpers.ex +++ b/lib/open_api_spex/json_api_helpers.ex @@ -7,21 +7,15 @@ defmodule OpenApiSpex.JsonApiHelpers do raise "%JsonApiDocument{} :title is required and must be a string" end - resource = %{ - document.resource - | title: document.resource.title || document.title <> "Resource" - } + resource = document.resource + resource_item_schema = resource_schema(resource) - resource_schema = %Schema{ - type: :object, - properties: %{ - id: %Schema{type: :string}, - type: %Schema{type: :string}, - attributes: attributes_schema(resource) - }, - required: [:id, :type, :attributes], - title: resource.title - } + 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, @@ -76,16 +70,6 @@ defmodule OpenApiSpex.JsonApiHelpers do end end - defmacro document(attrs) do - quote do - @resource struct!(OpenApiSpex.JsonApiHelpers.JsonApiResource, unquote(attrs)) - def resource, do: @resource - - @resource_schema OpenApiSpex.JsonApiHelpers.resource_schema(@resource) - def resource_schema, do: @resource_schema - end - end - defmacro generate_resource_schema(attrs) do quote do require OpenApiSpex diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs index f6e1e107..c751f256 100644 --- a/test/json_api_helpers_test.exs +++ b/test/json_api_helpers_test.exs @@ -1,7 +1,7 @@ defmodule OpenApiSpex.JsonApiHelpersTest do use ExUnit.Case, async: true - alias OpenApiSpexTest.{CartDocument, CartResource} + alias OpenApiSpexTest.{CartDocument, CartIndexDocument, CartResource} alias OpenApiSpex.{JsonApiHelpers, Schema} alias OpenApiSpex.JsonApiHelpers.JsonApiResource @@ -12,6 +12,14 @@ defmodule OpenApiSpex.JsonApiHelpersTest do 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.title == "CartResource" + end end describe "generate_resource_schema/1" do diff --git a/test/support/cart_document.ex b/test/support/cart_document.ex index 25e43780..ce9e5905 100644 --- a/test/support/cart_document.ex +++ b/test/support/cart_document.ex @@ -1,17 +1,11 @@ defmodule OpenApiSpexTest.CartDocument do alias OpenApiSpex.JsonApiHelpers - alias OpenApiSpex.JsonApiHelpers.JsonApiResource - alias OpenApiSpex.Schema + alias OpenApiSpexTest.CartResource require OpenApiSpex.JsonApiHelpers JsonApiHelpers.generate_document_schema( title: "Cart", - resource: %JsonApiResource{ - properties: %{ - total: %Schema{type: :integer} - }, - additionalProperties: false - } + 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..87ef2a06 --- /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: "CartIndex", + multiple: true, + resource: CartResource.resource() + ) +end From e17656f628d34190da1feb71c18a6c6494ac90d1 Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Mon, 15 Jun 2020 15:16:27 -0700 Subject: [PATCH 05/10] Refactor --- lib/open_api_spex/json_api_helpers.ex | 63 +++---------------- .../json_api_helpers/json_api_document.ex | 34 ++++++++++ .../json_api_helpers/json_api_resource.ex | 31 +++++++++ test/json_api_helpers_test.exs | 8 +-- 4 files changed, 77 insertions(+), 59 deletions(-) diff --git a/lib/open_api_spex/json_api_helpers.ex b/lib/open_api_spex/json_api_helpers.ex index 911bba8b..3ea34aab 100644 --- a/lib/open_api_spex/json_api_helpers.ex +++ b/lib/open_api_spex/json_api_helpers.ex @@ -1,69 +1,22 @@ defmodule OpenApiSpex.JsonApiHelpers do alias OpenApiSpex.JsonApiHelpers.{JsonApiDocument, JsonApiResource} - alias OpenApiSpex.Schema - def document_schema(%JsonApiDocument{} = 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 = resource_schema(resource) - - 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 <> "Document" - } - end - - def resource_schema(%JsonApiResource{} = 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" - } + def document_schema(document) do + JsonApiDocument.schema(document) end - def attributes_schema(%JsonApiResource{} = 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" - } + def resource_schema(resource) do + JsonApiResource.schema(resource) end defmacro generate_document_schema(attrs) do quote do require OpenApiSpex - @document struct!(OpenApiSpex.JsonApiHelpers.JsonApiDocument, unquote(attrs)) + @document struct!(JsonApiDocument, unquote(attrs)) def document, do: @document - @document_schema OpenApiSpex.JsonApiHelpers.document_schema(@document) + @document_schema JsonApiDocument.schema(@document) def document_schema, do: @document_schema OpenApiSpex.schema(@document_schema) @@ -74,10 +27,10 @@ defmodule OpenApiSpex.JsonApiHelpers do quote do require OpenApiSpex - @resource struct!(OpenApiSpex.JsonApiHelpers.JsonApiResource, unquote(attrs)) + @resource struct!(JsonApiResource, unquote(attrs)) def resource, do: @resource - @resource_schema OpenApiSpex.JsonApiHelpers.resource_schema(@resource) + @resource_schema JsonApiResource.schema(@resource) def resource_schema, do: @resource_schema OpenApiSpex.schema(@resource_schema) 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 index 025ddff3..7e8a2e2d 100644 --- a/lib/open_api_spex/json_api_helpers/json_api_document.ex +++ b/lib/open_api_spex/json_api_helpers/json_api_document.ex @@ -1,5 +1,39 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do + alias OpenApiSpex.Schema + alias OpenApiSpex.JsonApiHelpers.JsonApiResource + defstruct resource: nil, multiple: false, title: 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_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 <> "Document" + } + 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 index 9312f1c5..9dc4d433 100644 --- a/lib/open_api_spex/json_api_helpers/json_api_resource.ex +++ b/lib/open_api_spex/json_api_helpers/json_api_resource.ex @@ -1,6 +1,37 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiResource do + alias OpenApiSpex.Schema + defstruct additionalProperties: nil, properties: %{}, required: [], title: nil + + 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/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs index c751f256..864365b7 100644 --- a/test/json_api_helpers_test.exs +++ b/test/json_api_helpers_test.exs @@ -34,7 +34,7 @@ defmodule OpenApiSpex.JsonApiHelpersTest do test "attributes" do resource = CartResource.resource() schema = JsonApiHelpers.resource_schema(resource) - assert schema.properties.attributes == JsonApiHelpers.attributes_schema(resource) + assert schema.properties.attributes == JsonApiResource.attributes_schema(resource) assert %Schema{} = schema.properties.id assert %Schema{} = schema.properties.type end @@ -49,13 +49,13 @@ defmodule OpenApiSpex.JsonApiHelpersTest do describe "attributes_schema/1" do test "generates schema with same properties" do resource = CartResource.resource() - schema = JsonApiHelpers.attributes_schema(resource) + schema = JsonApiResource.attributes_schema(resource) assert schema.properties == resource.properties end test "generates title" do resource = CartResource.resource() - schema = JsonApiHelpers.attributes_schema(resource) + schema = JsonApiResource.attributes_schema(resource) assert schema.title == "CartAttributes" end @@ -67,7 +67,7 @@ defmodule OpenApiSpex.JsonApiHelpersTest do RuntimeError, "%JsonApiResource{} :title is required and must be a string", fn -> - JsonApiHelpers.attributes_schema(resource) + JsonApiResource.attributes_schema(resource) end ) end From 66d740933fd57e2b4a851a7817219ec86aad1e7c Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Mon, 15 Jun 2020 16:32:24 -0700 Subject: [PATCH 06/10] wip test JasonApiHelpers integration with operation specs --- test/json_api_helpers_test.exs | 10 +++++++ test/support/api_spec.ex | 4 +-- test/support/json_api_controller.ex | 43 +++++++++++++++++++++++++++++ test/support/router.ex | 2 ++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 test/support/json_api_controller.ex diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs index 864365b7..5b1efda9 100644 --- a/test/json_api_helpers_test.exs +++ b/test/json_api_helpers_test.exs @@ -5,6 +5,16 @@ defmodule OpenApiSpex.JsonApiHelpersTest do alias OpenApiSpex.{JsonApiHelpers, Schema} alias OpenApiSpex.JsonApiHelpers.JsonApiResource + describe "from operation specs" do + test "index action" do + spec = OpenApiSpexTest.ApiSpec.spec() + keys = Map.keys(spec.components.schemas) + IO.inspect(keys, label: "keys") + assert %Schema{} = schema = spec.components.schemas["CartIndexDocument"] + IO.inspect(schema, label: "schema") + end + end + describe "generate_resource_document/1" do test "generate schema/0" do assert %Schema{} = schema = CartDocument.schema() diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex index 1a7c1dd8..9210d376 100644 --- a/test/support/api_spec.ex +++ b/test/support/api_spec.ex @@ -35,7 +35,7 @@ defmodule OpenApiSpexTest.ApiSpec do }, components: %Components{ schemas: - for schemaMod <- [ + for schema_mod <- [ Schemas.Pet, Schemas.PetType, Schemas.Cat, @@ -46,7 +46,7 @@ defmodule OpenApiSpexTest.ApiSpec do Schemas.Primitive ], into: %{} do - schema = schemaMod.schema() + schema = schema_mod.schema() {schema.title, schema} end, parameters: %{ diff --git a/test/support/json_api_controller.ex b/test/support/json_api_controller.ex new file mode 100644 index 00000000..ecec12a0 --- /dev/null +++ b/test/support/json_api_controller.ex @@ -0,0 +1,43 @@ +defmodule OpenApiSpexTest.JsonApiController do + use Phoenix.Controller + use OpenApiSpex.Controller + + alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpexTest.CartResource + + @doc """ + Get a list of carts. + """ + @doc responses: [ + ok: { + "Carts", + "application/json", + OpenApiSpexTest.CartIndexDocument + # JsonApiHelpers.document_schema( + # title: "Carts", + # resource: CartResource.resource(), + # multiple: true + # ) + } + ] + 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/router.ex b/test/support/router.ex index 33cdf712..96e070cc 100644 --- a/test/support/router.ex +++ b/test/support/router.ex @@ -29,5 +29,7 @@ defmodule OpenApiSpexTest.Router do post "/utility/echo/body_params", UtilityController, :echo_body_params get "/json_render_error", JsonRenderErrorController, :index + get "/jsonapi/carts", JsonApiController, :index + get "/jsonapi/carts/:id", JsonApiController, :show end end From 41b8e06d1b41850e7b9cd81dfa0f147f2e777631 Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Mon, 15 Jun 2020 18:26:33 -0700 Subject: [PATCH 07/10] Working example --- .../json_api_helpers/json_api_document.ex | 18 +++++++++-- .../json_api_helpers/json_api_resource.ex | 4 +++ lib/open_api_spex/schema_resolver.ex | 7 +++++ test/json_api_helpers_test.exs | 9 ++---- test/support/api_spec_2.ex | 30 +++++++++++++++++++ test/support/cart_document.ex | 2 +- test/support/cart_index_document.ex | 4 +-- test/support/json_api_controller.ex | 14 +++++---- test/support/router_2.ex | 17 +++++++++++ 9 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 test/support/api_spec_2.ex create mode 100644 test/support/router_2.ex 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 index 7e8a2e2d..4c3c184c 100644 --- a/lib/open_api_spex/json_api_helpers/json_api_document.ex +++ b/lib/open_api_spex/json_api_helpers/json_api_document.ex @@ -4,7 +4,8 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do defstruct resource: nil, multiple: false, - title: nil + title: nil, + "x-struct": nil def schema(%__MODULE__{} = document) do if not is_binary(document.title) do @@ -14,9 +15,19 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do 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"} + %Schema{ + type: :array, + items: resource_item_schema, + title: resource_title <> "List" + } else resource_item_schema end @@ -27,7 +38,8 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do data: resource_schema }, required: [:data], - title: document.title <> "Document" + title: document.title, + "x-struct": document."x-struct" } 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 index 9dc4d433..4e572496 100644 --- a/lib/open_api_spex/json_api_helpers/json_api_resource.ex +++ b/lib/open_api_spex/json_api_helpers/json_api_resource.ex @@ -6,6 +6,10 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiResource do 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" diff --git a/lib/open_api_spex/schema_resolver.ex b/lib/open_api_spex/schema_resolver.ex index fa73ffd7..d558fc80 100644 --- a/lib/open_api_spex/schema_resolver.ex +++ b/lib/open_api_spex/schema_resolver.ex @@ -217,6 +217,13 @@ defmodule OpenApiSpex.SchemaResolver do properties: properties } + schemas = + if schema."x-struct" do + Map.put(schemas, schema.title, schema) + else + schemas + end + {schema, schemas} end diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs index 5b1efda9..1821e48d 100644 --- a/test/json_api_helpers_test.exs +++ b/test/json_api_helpers_test.exs @@ -7,11 +7,8 @@ defmodule OpenApiSpex.JsonApiHelpersTest do describe "from operation specs" do test "index action" do - spec = OpenApiSpexTest.ApiSpec.spec() - keys = Map.keys(spec.components.schemas) - IO.inspect(keys, label: "keys") - assert %Schema{} = schema = spec.components.schemas["CartIndexDocument"] - IO.inspect(schema, label: "schema") + spec = OpenApiSpexTest.ApiSpec2.spec() + assert %Schema{} = _schema = spec.components.schemas["CartIndexResponse"] end end @@ -28,7 +25,7 @@ defmodule OpenApiSpex.JsonApiHelpersTest do assert schema.title == "CartIndexDocument" assert %{data: _} = schema.properties assert schema.properties.data.type == :array - assert schema.properties.data.items.title == "CartResource" + assert schema.properties.data.items == OpenApiSpexTest.CartResource end 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 index ce9e5905..9a1d2af7 100644 --- a/test/support/cart_document.ex +++ b/test/support/cart_document.ex @@ -5,7 +5,7 @@ defmodule OpenApiSpexTest.CartDocument do require OpenApiSpex.JsonApiHelpers JsonApiHelpers.generate_document_schema( - title: "Cart", + title: "CartDocument", resource: CartResource.resource() ) end diff --git a/test/support/cart_index_document.ex b/test/support/cart_index_document.ex index 87ef2a06..2c7491a0 100644 --- a/test/support/cart_index_document.ex +++ b/test/support/cart_index_document.ex @@ -5,8 +5,8 @@ defmodule OpenApiSpexTest.CartIndexDocument do require OpenApiSpex.JsonApiHelpers JsonApiHelpers.generate_document_schema( - title: "CartIndex", + title: "CartIndexDocument", multiple: true, - resource: CartResource.resource() + resource: CartResource ) end diff --git a/test/support/json_api_controller.ex b/test/support/json_api_controller.ex index ecec12a0..c5937eb7 100644 --- a/test/support/json_api_controller.ex +++ b/test/support/json_api_controller.ex @@ -5,6 +5,8 @@ defmodule OpenApiSpexTest.JsonApiController do alias OpenApiSpex.JsonApiHelpers alias OpenApiSpexTest.CartResource + require OpenApiSpex.JsonApiHelpers + @doc """ Get a list of carts. """ @@ -12,12 +14,12 @@ defmodule OpenApiSpexTest.JsonApiController do ok: { "Carts", "application/json", - OpenApiSpexTest.CartIndexDocument - # JsonApiHelpers.document_schema( - # title: "Carts", - # resource: CartResource.resource(), - # multiple: true - # ) + JsonApiHelpers.document_schema( + title: "CartIndexResponse", + resource: CartResource, + multiple: true, + "x-struct": "CartIndexResponse" + ) } ] def index(conn, _params) do 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 From 15a0843570ca0f0ef38eb36bc50a821f5c820076 Mon Sep 17 00:00:00 2001 From: Moxley Stratton Date: Sat, 1 Aug 2020 08:16:01 -0700 Subject: [PATCH 08/10] wip --- lib/open_api_spex/open_api/schema_extension.ex | 3 +++ lib/open_api_spex/schema.ex | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 lib/open_api_spex/open_api/schema_extension.ex diff --git a/lib/open_api_spex/open_api/schema_extension.ex b/lib/open_api_spex/open_api/schema_extension.ex new file mode 100644 index 00000000..0ec87c20 --- /dev/null +++ b/lib/open_api_spex/open_api/schema_extension.ex @@ -0,0 +1,3 @@ +defmodule OpenApiSpex.OpenApi.SchemaExtension do + defstruct [:struct, :validate, registered: false] +end diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 1731e2bf..d6a456cf 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -183,7 +183,8 @@ defmodule OpenApiSpex.Schema do :example, :deprecated, :"x-struct", - :"x-validate" + :"x-validate", + :"x-register" ] @typedoc """ From 29ac389be3dbeb39a2d54fcb2762c58513e36ac3 Mon Sep 17 00:00:00 2001 From: Michael Ramstein Date: Mon, 21 Sep 2020 18:04:13 +0200 Subject: [PATCH 09/10] [skip ci] WIP Adds CastTests to ensure usability Moves CartModul defintions into JsonApiSchemas Module --- .../json_api_helpers/json_api_document.ex | 43 +++++++++- .../json_api_helpers/json_api_error.ex | 46 ++++++++++ test/json_api_helpers_test.exs | 5 ++ test/plug/cast_test.exs | 16 ++++ test/support/api_spec.ex | 3 +- test/support/api_spec_2.ex | 30 ------- test/support/cart_document.ex | 11 --- test/support/cart_index_document.ex | 12 --- test/support/cart_resource.ex | 14 --- test/support/json_api_controller.ex | 86 +++++++++++++++++-- test/support/json_api_schemas.ex | 41 +++++++++ test/support/router.ex | 4 +- 12 files changed, 229 insertions(+), 82 deletions(-) create mode 100644 lib/open_api_spex/json_api_helpers/json_api_error.ex delete mode 100644 test/support/api_spec_2.ex delete mode 100644 test/support/cart_document.ex delete mode 100644 test/support/cart_index_document.ex delete mode 100644 test/support/cart_resource.ex create mode 100644 test/support/json_api_schemas.ex 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 index 4c3c184c..cf900d57 100644 --- a/lib/open_api_spex/json_api_helpers/json_api_document.ex +++ b/lib/open_api_spex/json_api_helpers/json_api_document.ex @@ -4,6 +4,7 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do defstruct resource: nil, multiple: false, + paginated: false, title: nil, "x-struct": nil @@ -32,11 +33,20 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do resource_item_schema end + properties = %{ + data: resource_schema + } + + properties = + if document.paginated do + Map.merge(properties, pagination_spec()) + else + properties + end + %Schema{ type: :object, - properties: %{ - data: resource_schema - }, + properties: properties, required: [:data], title: document.title, "x-struct": document."x-struct" @@ -48,4 +58,31 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do |> struct!(document_attrs) |> schema() end + + def pagination_spec() do + # https://jsonapi.org/format/#fetching-pagination + # TODO: Links can be omitted or nullable, nullable should be delcared! + %{ + self: %Schema{ + type: :string, + description: "Link to this page of results" + }, + prev: %Schema{ + type: :string, + description: "Link to the previous page of results" + }, + next: %Schema{ + type: :string, + description: "Link to the next page of results" + }, + last: %Schema{ + type: :string, + description: "Link to the last page of results" + }, + first: %Schema{ + type: :string, + description: "Link to the first page of results" + } + } + end end diff --git a/lib/open_api_spex/json_api_helpers/json_api_error.ex b/lib/open_api_spex/json_api_helpers/json_api_error.ex new file mode 100644 index 00000000..d17ca9a7 --- /dev/null +++ b/lib/open_api_spex/json_api_helpers/json_api_error.ex @@ -0,0 +1,46 @@ +defmodule OpenApiSpex.JsonApiHelpers.JsonApiError do + alias OpenApiSpex.Schema + + @moduledoc """ + @see https://jsonapi.org/format/#errors + """ + + @behaviour OpenApiSpex.Schema + def schema do + %OpenApiSpex.Schema{ + title: "JsonApiError", + type: :object, + properties: %{ + id: %Schema{ + type: :string, + description: "A unique identifier for this particular occurrence of the problem." + }, + status: %Schema{ + type: :string, + description: + "The HTTP status code applicable to this problem, expressed as a string value." + }, + code: %Schema{ + type: :string, + description: "An application-specific error code, expressed as a string value." + }, + title: %Schema{ + type: :string, + description: + "A short, human-readable summary of the problem that *SHOULD NOT* change from occurrence to occurrence of the problem, except for purposes of localization." + }, + detail: %Schema{ + type: :string, + description: "A human-readable explanation specific to this occurrence of the problem." + } + # TODO: Props: + # links: %Schema{type: JsonApiLinks, description: "a links object containing the following members"}, + # source: %Schema{type: NEED_PROPPER_DEFINITION, description: "An object containing references to the source of the error."}, + # meta: %Schema{ + # type: NEED_PROPPER_DEFINITION, + # description: "A meta object containing non-standard meta-information about the error." + # } + } + } + end +end diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs index 1821e48d..015e3ab3 100644 --- a/test/json_api_helpers_test.exs +++ b/test/json_api_helpers_test.exs @@ -10,6 +10,11 @@ defmodule OpenApiSpex.JsonApiHelpersTest do spec = OpenApiSpexTest.ApiSpec2.spec() assert %Schema{} = _schema = spec.components.schemas["CartIndexResponse"] end + + test "show action" do + spec = OpenApiSpexTest.ApiSpec2.spec() |> IO.inspect() + assert %Schema{} = _schema = spec.components.schemas["ShowCartResponse"] + end end describe "generate_resource_document/1" do diff --git a/test/plug/cast_test.exs b/test/plug/cast_test.exs index 8e38c5e7..0d8cbe52 100644 --- a/test/plug/cast_test.exs +++ b/test/plug/cast_test.exs @@ -318,4 +318,20 @@ defmodule OpenApiSpex.Plug.CastTest do assert Jason.decode!(conn.resp_body) == %{"_json" => [%{"one" => "this"}]} end end + + describe "json-api schemas" do + test "create cart" do + request_body = %{"data" => %{"attributes" => %{"total" => "200"}}} + + conn = + :post + |> Plug.Test.conn("api/jsonapi/carts", Jason.encode!(request_body)) + |> Plug.Conn.put_req_header("content-type", "application/json") + |> OpenApiSpexTest.Router.call([]) + + # Phoenix parses json body params that start with array as structs starting with _json key + # https://hexdocs.pm/plug/Plug.Parsers.JSON.html + assert Jason.decode!(conn.resp_body) == %{"_json" => [%{"one" => "this"}]} + end + end end diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex index 9210d376..d57a5fb2 100644 --- a/test/support/api_spec.ex +++ b/test/support/api_spec.ex @@ -8,7 +8,8 @@ defmodule OpenApiSpexTest.ApiSpec do Info, Components, Parameter, - Schema + Schema, + JsonApiSchemas } alias OpenApiSpexTest.{Router, Schemas} diff --git a/test/support/api_spec_2.ex b/test/support/api_spec_2.ex deleted file mode 100644 index 4fcfecdb..00000000 --- a/test/support/api_spec_2.ex +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 9a1d2af7..00000000 --- a/test/support/cart_document.ex +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2c7491a0..00000000 --- a/test/support/cart_index_document.ex +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index c8818bd0..00000000 --- a/test/support/cart_resource.ex +++ /dev/null @@ -1,14 +0,0 @@ -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/json_api_controller.ex b/test/support/json_api_controller.ex index c5937eb7..c342a72f 100644 --- a/test/support/json_api_controller.ex +++ b/test/support/json_api_controller.ex @@ -3,43 +3,111 @@ defmodule OpenApiSpexTest.JsonApiController do use OpenApiSpex.Controller alias OpenApiSpex.JsonApiHelpers - alias OpenApiSpexTest.CartResource + + alias OpenApiSpexTest.JsonApiSchemas require OpenApiSpex.JsonApiHelpers + plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true + + @resource JsonApiSchemas.CartResource + @resource_document JsonApiSchemas.CartDocument + @doc """ Get a list of carts. """ + @doc responses: [ + ok: { + "Carts", + "application/json", + JsonApiSchemas.CartListDocument + } + ] + def index(conn, _params) do + json(conn, %{data: []}) + end + + @doc """ + Get a list of carts, but paginated. + """ @doc responses: [ ok: { "Carts", "application/json", JsonApiHelpers.document_schema( title: "CartIndexResponse", - resource: CartResource, - multiple: true, - "x-struct": "CartIndexResponse" + resource: @resource, + multiple: true ) } ] - def index(conn, _params) do + def paged_index(conn, _params) do json(conn, %{data: []}) end @doc """ - Get a cart by ID. + Get a list of carts, but defined in annotation. """ @doc responses: [ ok: { - "Cart", + "Carts", "application/json", JsonApiHelpers.document_schema( - title: "Cart", - resource: CartResource.resource() + title: "CartIndexResponse", + resource: @resource, + multiple: true ) } ] + def annotated_index(conn, _params) do + json(conn, %{data: []}) + end + + @doc """ + Create a Cart. + """ + @doc + @doc request_body: {"CartDocument", "application/json", @resource_document}, + responses: [ + created: {"CartDocument", "application/json", @resource_document} + ] + def create(conn, _params) do + json(conn, %@resource_document{data: %@resource{}}) + end + + @doc """ + Get a cart by ID. + """ + @doc responses: [ + ok: { + "Cart", + "application/json", + @resource_document + } + ] def show(conn, _params) do json(conn, %{data: %{}}) end + + @doc """ + Updating a Cart's Attributes + + This action is available as a PUT or a PATCH. + """ + @doc request_body: + {"The cart attributes", "application/json", @resource_document, required: true}, + responses: [ + created: {"Car", "application/json", @resource_document} + ] + def update(conn, _) do + json(conn, %@resource_document{ + data: %@resource{ + id: "123", + type: "cart", + attributes: %{ + total: "4000" + } + } + }) + end end diff --git a/test/support/json_api_schemas.ex b/test/support/json_api_schemas.ex new file mode 100644 index 00000000..7b5d5251 --- /dev/null +++ b/test/support/json_api_schemas.ex @@ -0,0 +1,41 @@ +defmodule OpenApiSpexTest.JsonApiSchemas do + alias OpenApiSpex.JsonApiHelpers + alias OpenApiSpex.Schema + alias OpenApiSpex.JsonApiHelpers.JsonApiDocument + alias OpenApiSpex.JsonApiHelpers.JsonApiResource + + require OpenApiSpex.JsonApiHelpers + + defmodule CartResource do + JsonApiHelpers.generate_resource_schema( + title: "Cart", + properties: %{ + total: %Schema{type: :integer} + }, + additionalProperties: false + ) + end + + defmodule CartDocument do + JsonApiHelpers.generate_document_schema( + title: "CartDocument", + resource: CartResource + ) + end + + defmodule CartListDocument do + JsonApiHelpers.generate_document_schema( + title: "CartListDocument", + paginated: true, + resource: CartResource + ) + end + + defmodule CartPaginatedDocument do + JsonApiHelpers.generate_document_schema( + title: "CartPaginatedDocument", + paginated: true, + resource: CartResource + ) + end +end diff --git a/test/support/router.ex b/test/support/router.ex index 96e070cc..db5dd522 100644 --- a/test/support/router.ex +++ b/test/support/router.ex @@ -29,7 +29,7 @@ defmodule OpenApiSpexTest.Router do post "/utility/echo/body_params", UtilityController, :echo_body_params get "/json_render_error", JsonRenderErrorController, :index - get "/jsonapi/carts", JsonApiController, :index - get "/jsonapi/carts/:id", JsonApiController, :show + + resources "/jsonapi/carts", JsonApiController, only: [:create, :index, :show] end end From dc8e1a4a3e73c50516f4ef1a13305df79d47b96b Mon Sep 17 00:00:00 2001 From: Michael Ramstein Date: Tue, 22 Sep 2020 17:16:30 +0200 Subject: [PATCH 10/10] Adds tests for pagination links --- .../json_api_helpers/json_api_document.ex | 51 +++++++++++-------- .../json_api_helpers/json_api_resource.ex | 3 +- test/json_api_helpers_test.exs | 48 +++++++++++------ test/plug/cast_test.exs | 42 +++++++++++++-- test/support/api_spec.ex | 3 +- .../{ => controllers}/json_api_controller.ex | 51 ++++++++++++++----- test/support/json_api_schemas.ex | 13 +++++ test/support/router.ex | 2 + 8 files changed, 156 insertions(+), 57 deletions(-) rename test/support/{ => controllers}/json_api_controller.ex (64%) 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 index cf900d57..736b1522 100644 --- a/lib/open_api_spex/json_api_helpers/json_api_document.ex +++ b/lib/open_api_spex/json_api_helpers/json_api_document.ex @@ -23,7 +23,7 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do end resource_schema = - if document.multiple do + if document.multiple || document.paginated do %Schema{ type: :array, items: resource_item_schema, @@ -39,7 +39,7 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do properties = if document.paginated do - Map.merge(properties, pagination_spec()) + Map.put(properties, :links, pagination_spec()) else properties end @@ -62,26 +62,33 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do def pagination_spec() do # https://jsonapi.org/format/#fetching-pagination # TODO: Links can be omitted or nullable, nullable should be delcared! - %{ - self: %Schema{ - type: :string, - description: "Link to this page of results" - }, - prev: %Schema{ - type: :string, - description: "Link to the previous page of results" - }, - next: %Schema{ - type: :string, - description: "Link to the next page of results" - }, - last: %Schema{ - type: :string, - description: "Link to the last page of results" - }, - first: %Schema{ - type: :string, - description: "Link to the first page of results" + %Schema{ + type: :object, + properties: %{ + prev: %Schema{ + type: :string, + description: "Link to the previous page of results", + nullable: true, + readOnly: true + }, + next: %Schema{ + type: :string, + description: "Link to the next page of results", + nullable: true, + readOnly: true + }, + last: %Schema{ + type: :string, + description: "Link to the last page of results", + nullable: true, + readOnly: true + }, + first: %Schema{ + type: :string, + description: "Link to the first page of results", + nullable: true, + readOnly: true + } } } 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 index 4e572496..35db7406 100644 --- a/lib/open_api_spex/json_api_helpers/json_api_resource.ex +++ b/lib/open_api_spex/json_api_helpers/json_api_resource.ex @@ -22,7 +22,8 @@ defmodule OpenApiSpex.JsonApiHelpers.JsonApiResource do type: %Schema{type: :string}, attributes: attributes_schema(resource) }, - required: [:id, :type], + # For responses, :id must be required too + required: [:type], title: resource.title <> "Resource" } end diff --git a/test/json_api_helpers_test.exs b/test/json_api_helpers_test.exs index 015e3ab3..6c8d4800 100644 --- a/test/json_api_helpers_test.exs +++ b/test/json_api_helpers_test.exs @@ -1,36 +1,52 @@ defmodule OpenApiSpex.JsonApiHelpersTest do use ExUnit.Case, async: true - alias OpenApiSpexTest.{CartDocument, CartIndexDocument, CartResource} + alias OpenApiSpexTest.JsonApiSchemas.{ + CartResource, + CartDocument, + CartPaginatedDocument, + CartListDocument + } + 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 - - test "show action" do - spec = OpenApiSpexTest.ApiSpec2.spec() |> IO.inspect() - assert %Schema{} = _schema = spec.components.schemas["ShowCartResponse"] - end - end + # describe "from operation specs" do + # test "index action" do + # spec = OpenApiSpexTest.ApiSpec.spec() + # assert %Schema{} = _schema = spec.components.schemas["CartAnnotatedDocument"] + # 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 + assert schema.properties.data.schema().title == CartResource.schema().title end test "generate schema for index document" do - assert %Schema{} = schema = CartIndexDocument.schema() - assert schema.title == "CartIndexDocument" + assert %Schema{} = schema = CartListDocument.schema() + assert schema.title == "CartListDocument" assert %{data: _} = schema.properties assert schema.properties.data.type == :array - assert schema.properties.data.items == OpenApiSpexTest.CartResource + assert schema.properties.data.items == CartResource + end + + test "generate schema for paginated document" do + assert %Schema{} = schema = CartPaginatedDocument.schema() + assert schema.title == "CartPaginatedDocument" + assert %{data: _} = schema.properties + assert schema.properties.data.type == :array + assert schema.properties.data.items == CartResource + + # Check for meta fields + assert %{links: links} = schema.properties + assert links.properties.first.type == :string + assert links.properties.last.type == :string + assert links.properties.prev.type == :string + assert links.properties.next.type == :string end end diff --git a/test/plug/cast_test.exs b/test/plug/cast_test.exs index 0d8cbe52..d730f28c 100644 --- a/test/plug/cast_test.exs +++ b/test/plug/cast_test.exs @@ -321,7 +321,13 @@ defmodule OpenApiSpex.Plug.CastTest do describe "json-api schemas" do test "create cart" do - request_body = %{"data" => %{"attributes" => %{"total" => "200"}}} + request_body = %{ + "data" => %{ + "attributes" => %{"total" => 200}, + "type" => "cart", + "id" => "492e7435-cd23-4ce0-a189-edf7fdc4b490" + } + } conn = :post @@ -329,9 +335,37 @@ defmodule OpenApiSpex.Plug.CastTest do |> Plug.Conn.put_req_header("content-type", "application/json") |> OpenApiSpexTest.Router.call([]) - # Phoenix parses json body params that start with array as structs starting with _json key - # https://hexdocs.pm/plug/Plug.Parsers.JSON.html - assert Jason.decode!(conn.resp_body) == %{"_json" => [%{"one" => "this"}]} + assert Jason.decode!(conn.resp_body) == request_body + end + + test "list carts" do + conn = + :get + |> Plug.Test.conn("api/jsonapi/carts") + |> OpenApiSpexTest.Router.call([]) + + assert Jason.decode!(conn.resp_body) == %{"data" => []} + end + + test "paginated carts" do + conn = + :get + |> Plug.Test.conn("api/jsonapi/carts-paginated?page[number]=1&page[size]=10") + |> OpenApiSpexTest.Router.call([]) + + assert Jason.decode!(conn.resp_body) == %{ + "data" => [], + "links" => %{ + "first" => + "https://example.com/api/jsonapi/carts-paginated?page[number]=1&page[size]=10", + "last" => + "https://example.com/api/jsonapi/carts-paginated?page[number]=10&page[size]=10", + "next" => + "https://example.com/api/jsonapi/carts-paginated?page[number]=2&page[size]=10", + "prev" => + "https://example.com/api/jsonapi/carts-paginated?page[number]=0&page[size]=10" + } + } end end end diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex index d57a5fb2..9210d376 100644 --- a/test/support/api_spec.ex +++ b/test/support/api_spec.ex @@ -8,8 +8,7 @@ defmodule OpenApiSpexTest.ApiSpec do Info, Components, Parameter, - Schema, - JsonApiSchemas + Schema } alias OpenApiSpexTest.{Router, Schemas} diff --git a/test/support/json_api_controller.ex b/test/support/controllers/json_api_controller.ex similarity index 64% rename from test/support/json_api_controller.ex rename to test/support/controllers/json_api_controller.ex index c342a72f..ff18add4 100644 --- a/test/support/json_api_controller.ex +++ b/test/support/controllers/json_api_controller.ex @@ -30,21 +30,39 @@ defmodule OpenApiSpexTest.JsonApiController do @doc """ Get a list of carts, but paginated. """ - @doc responses: [ + @doc parameters: [ + page: [in: :query, type: JsonApiSchemas.PageBasedPaginationParameter, required: true] + ], + responses: [ ok: { "Carts", "application/json", - JsonApiHelpers.document_schema( - title: "CartIndexResponse", - resource: @resource, - multiple: true - ) + JsonApiSchemas.CartPaginatedDocument } ] - def paged_index(conn, _params) do - json(conn, %{data: []}) + def paginated_index(conn, %{page: page}) do + page_params = Map.from_struct(page) + uri = %URI{scheme: "https", host: "example.com"} + + paged_url_fn = + &OpenApiSpexTest.Router.Helpers.json_api_url(uri, :paginated_index, + page: %{page_params | number: &1} + ) + + response = %{ + data: [], + links: %{ + first: paged_url_fn.(1), + last: paged_url_fn.(10), + prev: paged_url_fn.(page.number - 1), + next: paged_url_fn.(page.number + 1) + } + } + + json(conn, response) end + @spec annotated_index(Plug.Conn.t(), any) :: Plug.Conn.t() @doc """ Get a list of carts, but defined in annotation. """ @@ -53,7 +71,7 @@ defmodule OpenApiSpexTest.JsonApiController do "Carts", "application/json", JsonApiHelpers.document_schema( - title: "CartIndexResponse", + title: "CartAnnotatedDocument", resource: @resource, multiple: true ) @@ -66,13 +84,22 @@ defmodule OpenApiSpexTest.JsonApiController do @doc """ Create a Cart. """ - @doc @doc request_body: {"CartDocument", "application/json", @resource_document}, responses: [ created: {"CartDocument", "application/json", @resource_document} ] - def create(conn, _params) do - json(conn, %@resource_document{data: %@resource{}}) + def create(%Plug.Conn{body_params: payload} = conn, _params) do + response = %{ + data: %JsonApiSchemas.CartResource{ + id: "492e7435-cd23-4ce0-a189-edf7fdc4b490", + type: "cart", + attributes: payload.data.attributes + } + } + + conn + |> put_status(:created) + |> json(response) end @doc """ diff --git a/test/support/json_api_schemas.ex b/test/support/json_api_schemas.ex index 7b5d5251..4d7315a2 100644 --- a/test/support/json_api_schemas.ex +++ b/test/support/json_api_schemas.ex @@ -16,6 +16,19 @@ defmodule OpenApiSpexTest.JsonApiSchemas do ) end + defmodule PageBasedPaginationParameter do + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "PageBasedPaginationParameter", + type: :object, + properties: %{ + number: %Schema{type: :integer}, + size: %Schema{type: :integer} + } + }) + end + defmodule CartDocument do JsonApiHelpers.generate_document_schema( title: "CartDocument", diff --git a/test/support/router.ex b/test/support/router.ex index db5dd522..0e064ee9 100644 --- a/test/support/router.ex +++ b/test/support/router.ex @@ -31,5 +31,7 @@ defmodule OpenApiSpexTest.Router do get "/json_render_error", JsonRenderErrorController, :index resources "/jsonapi/carts", JsonApiController, only: [:create, :index, :show] + get "/jsonapi/carts-paginated", JsonApiController, :paginated_index + get "/jsonapi/carts-annotated", JsonApiController, :annotated_index end end