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

Proof of concept Json-API with OpenAPI support (take 2) #59

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ config :plug_content_security_policy,
worker_src: ~w('self')
}

# Mime type for JSON:API routes
config :mime, :types, %{
"application/vnd.api+json" => ["json"]
}

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
8 changes: 8 additions & 0 deletions lib/ash_hq/docs/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule AshHq.Docs do
use Ash.Api,
extensions: [
AshGraphql.Api,
AshJsonApi.Api,
AshAdmin.Api
]

Expand All @@ -20,4 +21,11 @@ defmodule AshHq.Docs do
resources do
registry AshHq.Docs.Registry
end

json_api do
prefix "/json_api"
serve_schema? true
open_api {AshHq.Docs.OpenApi, :spec, []}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used an MFA here as the most flexible option, but I'm not sure if it feels like idiomatic Ash?
Just the module name would work, as long as it exposes a spec/0 callback.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "ash way" is typically {:spark_behaviour, BehaviourName}, which will allow them to pass Module or {Module, opts}, and you'll always get {Module, opts} for the value, at which point you can say Module.spec(opts).

log_errors? true
end
end
35 changes: 35 additions & 0 deletions lib/ash_hq/docs/open_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule AshHq.Docs.OpenApi do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indicative of the kind of module Ash users would need to write for the outline of their OpenAPI document.

I like that this gives the users a chance to add extra schemas, tags, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Its a bit boilerplate-y, but its also very likely that people will eventually want to add their own custom endpoints and things like that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll want these functions to take lists of APIs if they don't already. Then they can set @apis in the top of the file. I'd also consider something like this:

    %OpenApi{
      info: %Info{
        title: "AshHQ Docs API",
        version: "1.1"
     },
     servers: [Server.from_endpoint(AshHqWeb.Endpoint]
    }
    |> AshJsonApi.OpenApi.extend_open_api_spex(@apis)

Either way, this is likely nitpicking as I think you've found a good balance between "boilerplate" and extensibility.

alias OpenApiSpex.{Info, OpenApi, Server, SecurityScheme}

def spec do
%OpenApi{
info: %Info{
title: "AshHQ Docs API",
version: "1.1"
},
servers: [
Server.from_endpoint(AshHqWeb.Endpoint)
],
paths: AshJsonApi.OpenApi.paths(AshHq.Docs),
tags: AshJsonApi.OpenApi.tags(AshHq.Docs),
components: %{
responses: AshJsonApi.OpenApi.responses(),
schemas: AshJsonApi.OpenApi.schemas(AshHq.Docs),
securitySchemes: %{
"api_key" => %SecurityScheme{
type: "apiKey",
description: "API Key provided in the Authorization header",
name: "api_key",
in: "header"
}
}
},
security: [
%{
# API Key security applies to all operations
"api_key" => []
}
]
}
end
end
17 changes: 17 additions & 0 deletions lib/ash_hq/docs/resources/guide/guide.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule AshHq.Docs.Guide do
AshHq.Docs.Extensions.Search,
AshHq.Docs.Extensions.RenderMarkdown,
AshGraphql.Resource,
AshJsonApi.Resource,
AshAdmin.Resource
]

Expand All @@ -17,6 +18,19 @@ defmodule AshHq.Docs.Guide do
end
end

json_api do
type "guide"
includes [library_version: [:library]]

routes do
base "/guides"

get :read
index :read
index :read_for_version, route: "/for-version"
end
end

admin do
form do
field :text do
Expand Down Expand Up @@ -52,9 +66,12 @@ defmodule AshHq.Docs.Guide do
end

read :read_for_version do
description "Query for Guides given a list of library versions to allow"

argument :library_versions, {:array, :uuid} do
allow_nil? false
constraints max_length: 20, min_length: 1
description "UUIDs of the library versions to allow in the query"
end

pagination offset?: true, countable: true, default_limit: 25, required?: false
Expand Down
15 changes: 15 additions & 0 deletions lib/ash_hq_web/docs_json_api_router.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule AshHqWeb.DocsJsonApiRouter do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made some modifications to the AshJsonApi.Api.Router to allow additional routes to be defined, for things like SwaggerUI / ReDoc. These can also go in the Phoenix Router, I just thought it was tidy to put all the JsonAPI/OpenAPI related things in one file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smart 👍🏻

use AshJsonApi.Api.Router, api: AshHq.Docs, registry: AshHq.Docs.Registry

get "/swaggerui",
to: OpenApiSpex.Plug.SwaggerUI,
init_opts: [
path: "/json_api/openapi",
title: "AshHQ JSON-API - Swagger UI",
default_model_expand_depth: 4
]

get "/redoc",
to: Redoc.Plug.RedocUI,
init_opts: [spec_url: "/json_api/openapi"]
end
2 changes: 1 addition & 1 deletion lib/ash_hq_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ defmodule AshHqWeb.Router do

## Api routes
scope "/" do
forward "/json_api", AshHqWeb.DocsJsonApiRouter
forward "/gql", Absinthe.Plug, schema: AshHqWeb.Schema

forward "/playground",
Absinthe.Plug.GraphiQL,
schema: AshHqWeb.Schema,
Expand Down
5 changes: 4 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule AshHq.MixProject do
{:ash_admin, github: "ash-project/ash_admin"},
{:ash_phoenix, github: "ash-project/ash_phoenix", override: true},
{:ash_graphql, github: "ash-project/ash_graphql"},
{:ash_json_api, github: "ash-project/ash_json_api"},
{:ash_json_api, github: "team-alembic/ash_json_api", branch: "feat/open-api-spex-helpers"},
{:absinthe_plug, "~> 1.5"},
{:ash_blog, github: "ash-project/ash_blog"},
{:ash_csv, github: "ash-project/ash_csv"},
Expand Down Expand Up @@ -70,6 +70,9 @@ defmodule AshHq.MixProject do
{:phoenix_live_dashboard, "~> 0.6"},
{:ecto_psql_extras, "~> 0.6"},
{:phoenix_ecto, "~> 4.4"},
# OpenApiSpex / SwaggerUI / Redoc
{:open_api_spex, "~> 3.16"},
{:redoc_ui_plug, github: "team-alembic/redoc_ui_plug"},
# Phoenix/Core dependencies
{:phoenix, "~> 1.6.14"},
{:ecto_sql, "~> 3.6"},
Expand Down
4 changes: 3 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"ash_blog": {:git, "https://github.com/ash-project/ash_blog.git", "79d1b9077b2712d4d4651eaa36c8e63119efd565", []},
"ash_csv": {:git, "https://github.com/ash-project/ash_csv.git", "bad0f6961bf5d135450dacda687d9df0549e80ae", []},
"ash_graphql": {:git, "https://github.com/ash-project/ash_graphql.git", "fb7b60f9e1793c912e238043aa115cd6585c19b7", []},
"ash_json_api": {:git, "https://github.com/ash-project/ash_json_api.git", "2525172e72d0e8b0e369155f32e5edfbf25b9691", []},
"ash_json_api": {:git, "https://github.com/team-alembic/ash_json_api.git", "22a3fc6ef59ac6aec93e6af15125eeabc037300c", [branch: "feat/open-api-spex-helpers"]},
"ash_phoenix": {:git, "https://github.com/ash-project/ash_phoenix.git", "5ffcd1916ece639c684273e632cc0aecebe82f26", []},
"ash_postgres": {:hex, :ash_postgres, "1.1.1", "2bbc2b39d9e387f89b964b29b042f88dd352b71e486d9aea7f9390ab1db3ced4", [:mix], [{:ash, "~> 2.1", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "fe47a6e629b6b23ce17c1d70b1bd4b3fd732df513b67126514fb88be86a6439e"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
Expand Down Expand Up @@ -70,6 +70,7 @@
"nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"open_api_spex": {:hex, :open_api_spex, "3.16.0", "9843af4e87550cd8ac5821b10e4c74f1d51f0d4e3310f824d780614743423b25", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "bb0be24a648b73e8fc8cbda17f514b8486262275e8b33e8b5ae66283df972129"},
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"},
Expand All @@ -90,6 +91,7 @@
"premailex": {:hex, :premailex, "0.3.16", "25c0c9c969f0025bbfdb06834f8f0fbd46e5ec50f5c252e6492165802ffbd2a6", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:floki, "~> 0.19", [hex: :floki, repo: "hexpm", optional: false]}, {:meeseeks, "~> 0.11", [hex: :meeseeks, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "c6b042f89ca63025dfbe3ef54fdbbe9d5f043b7c33d8e58f43a41d13a9475111"},
"providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"redoc_ui_plug": {:git, "https://github.com/team-alembic/redoc_ui_plug.git", "19d2b85f6ae122e08fdf37b7b2c1cd170ac846c8", []},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"},
"spark": {:hex, :spark, "0.2.8", "adce1887be100fae124c65f9dcfb6675fefdcd0ffd6355bb02f638bf88e26630", [:mix], [{:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "1619dc8fd899980e33a7a63939e0a02783ed32c3344d192ff6669bf44ff637a6"},
Expand Down