Skip to content

Json api helpers #289

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

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
39 changes: 39 additions & 0 deletions lib/open_api_spex/json_api_helpers.ex
Original file line number Diff line number Diff line change
@@ -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
95 changes: 95 additions & 0 deletions lib/open_api_spex/json_api_helpers/json_api_document.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
defmodule OpenApiSpex.JsonApiHelpers.JsonApiDocument do
alias OpenApiSpex.Schema
alias OpenApiSpex.JsonApiHelpers.JsonApiResource

defstruct resource: nil,
multiple: false,
paginated: 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 || document.paginated do
%Schema{
type: :array,
items: resource_item_schema,
title: resource_title <> "List"
}
else
resource_item_schema
end

properties = %{
data: resource_schema
}

properties =
if document.paginated do
Map.put(properties, :links, pagination_spec())
else
properties
end

%Schema{
type: :object,
properties: properties,
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

def pagination_spec() do
# https://jsonapi.org/format/#fetching-pagination
# TODO: Links can be omitted or nullable, nullable should be delcared!
%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
end
46 changes: 46 additions & 0 deletions lib/open_api_spex/json_api_helpers/json_api_error.ex
Original file line number Diff line number Diff line change
@@ -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
42 changes: 42 additions & 0 deletions lib/open_api_spex/json_api_helpers/json_api_resource.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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)
},
# For responses, :id must be required too
required: [: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
3 changes: 3 additions & 0 deletions lib/open_api_spex/open_api/schema_extension.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule OpenApiSpex.OpenApi.SchemaExtension do
defstruct [:struct, :validate, registered: false]
end
3 changes: 2 additions & 1 deletion lib/open_api_spex/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ defmodule OpenApiSpex.Schema do
:example,
:deprecated,
:"x-struct",
:"x-validate"
:"x-validate",
:"x-register"
]

@typedoc """
Expand Down
7 changes: 7 additions & 0 deletions lib/open_api_spex/schema_resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
103 changes: 103 additions & 0 deletions test/json_api_helpers_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
defmodule OpenApiSpex.JsonApiHelpersTest do
use ExUnit.Case, async: true

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.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.schema().title == CartResource.schema().title
end

test "generate schema for index document" do
assert %Schema{} = schema = CartListDocument.schema()
assert schema.title == "CartListDocument"
assert %{data: _} = schema.properties
assert schema.properties.data.type == :array
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

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
Loading