Skip to content

Commit

Permalink
[JET-2027] deref 抽象 (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
vanppo authored Oct 16, 2024
1 parent fe334ca commit 7e795e9
Show file tree
Hide file tree
Showing 38 changed files with 398 additions and 154 deletions.
2 changes: 2 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Config

config :resource_kit, ResourceKit.Deref, adapter: ResourceKit.Deref.Local

config :resource_kit, ResourceKitCLI.Endpoint, server: true
3 changes: 3 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Config

config :resource_kit, ResourceKit.Deref,
adapter: {ResourceKit.Deref.Local, directory: "test/fixtures"}

config :resource_kit, ResourceKit.Repo,
hostname: "localhost",
database: "resource_kit_test",
Expand Down
4 changes: 2 additions & 2 deletions lib/resource_kit.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule ResourceKit do
@moduledoc false

defdelegate insert(action, params), to: ResourceKit.Action.Insert, as: :run
defdelegate insert(action, params, opts), to: ResourceKit.Action.Insert, as: :run

defdelegate list(action, params), to: ResourceKit.Action.List, as: :run
defdelegate list(action, params, opts), to: ResourceKit.Action.List, as: :run
end
20 changes: 13 additions & 7 deletions lib/resource_kit/action/skeleton.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,29 @@ defmodule ResourceKit.Action.Skeleton do
alias ResourceKit.Pipeline.Compile.Token, as: CompileToken
alias ResourceKit.Pipeline.Execute.Token, as: ExecuteToken

def run(action, params) do
with {:ok, %{action: action, references: references}} <- __compile__(action) do
__execute__(action, references, params)
def run(action, params, opts) do
with {:ok, %{action: action, references: references}} <- __compile__(action, opts) do
__execute__(action, references, params, opts)
end
end

defp __compile__(action) do
%CompileToken{action: action}
defp __compile__(action, opts) do
root = Keyword.fetch!(opts, :root)
context = %CompileToken.Context{root: root, current: root}

%CompileToken{action: action, context: context}
|> Pluggable.run([&__MODULE__.compile(&1, [])])
|> case do
%CompileToken{halted: false} = token -> {:ok, token.assigns}
%CompileToken{errors: [reason]} -> {:error, reason}
end
end

defp __execute__(action, references, params) do
%ExecuteToken{action: action, references: references, params: params}
defp __execute__(action, references, params, opts) do
root = Keyword.fetch!(opts, :root)
context = %ExecuteToken.Context{root: root, current: root}

%ExecuteToken{action: action, references: references, params: params, context: context}
|> Pluggable.run([&__MODULE__.execute(&1, [])])
|> case do
%ExecuteToken{halted: false} = token -> ExecuteToken.fetch_assign(token, :result)
Expand Down
67 changes: 67 additions & 0 deletions lib/resource_kit/deref.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule ResourceKit.Deref do
@moduledoc """
A behavior definition that developers can use to implement their own dereferencing logic.
## Options
* `adapter` - A module that implemented the deref behaviour. Or `{adapter, opts}` if the adapter has options.
"""

alias ResourceKit.Types

alias ResourceKit.Deref.Context
alias ResourceKit.Schema.Ref

@conf Application.compile_env(:resource_kit, [__MODULE__, :adapter], ResourceKit.Deref.Local)
@adapter if is_tuple(@conf), do: elem(@conf, 0), else: @conf
@opts if is_tuple(@conf), do: elem(@conf, 1), else: []

@callback resolve(ref :: Ref.t(), ctx :: Context.t()) ::
{:ok, Ref.t()} | {:error, Types.error()}

@callback fetch(ref :: Ref.t(), ctx :: Context.t()) ::
{:ok, Types.json_value()} | {:error, Types.error()}

defmacro __using__(_args) do
quote location: :keep do
@behaviour unquote(__MODULE__)

import unquote(__MODULE__)

@impl unquote(__MODULE__)
def resolve(ref, ctx) do
unquote(__MODULE__).absolute(ref, ctx)
end

defoverridable resolve: 2
end
end

defguard is_absolute(term) when is_struct(term, Ref) and is_binary(term.uri.scheme)

@spec absolute(ref :: Ref.t(), ctx :: Context.t()) :: {:ok, Ref.t()} | {:error, Types.error()}
def absolute(ref, ctx)

def absolute(%Ref{} = ref, %Context{}) when is_absolute(ref) do
{:ok, ref}
end

def absolute(%Ref{uri: uri}, %Context{current: %Ref{uri: current}}) do
{:ok, %Ref{uri: %{current | path: Path.expand(uri.path, current.path)}}}
end

@spec resolve(ref :: Ref.t(), ctx :: Context.t()) :: {:ok, Ref.t()} | {:error, Types.error()}
def resolve(ref, ctx) do
@adapter.resolve(ref, put_options(ctx))
end

@spec fetch(ref :: Ref.t(), ctx :: Context.t()) ::
{:ok, Types.json_value()} | {:error, Types.error()}
def fetch(ref, ctx) do
@adapter.fetch(ref, put_options(ctx))
end

defp put_options(ctx) do
%{ctx | opts: @opts}
end
end
12 changes: 12 additions & 0 deletions lib/resource_kit/deref/context.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule ResourceKit.Deref.Context do
@moduledoc false

use TypedStruct

alias ResourceKit.Schema.Ref

typedstruct do
field :current, Ref.t(), enforce: true
field :opts, keyword(), default: []
end
end
41 changes: 41 additions & 0 deletions lib/resource_kit/deref/local.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule ResourceKit.Deref.Local do
@moduledoc """
A deref implementation that loads a JSON document from a local directory.
## Options
* `directory` - The root directory from which to load JSON documents. Defaults to `File.cwd/0`.
"""

use ResourceKit.Deref

alias ResourceKit.Deref.Context
alias ResourceKit.Schema.Ref

@impl ResourceKit.Deref
def fetch(%Ref{uri: %URI{} = uri}, %Context{opts: opts}) do
directory = Keyword.get_lazy(opts, :directory, &File.cwd!/0)
file = uri.path |> Path.relative() |> Path.expand(directory)

with {:ok, content} <- fetch_file(file),
{:ok, value} <- decode_json(content) do
{:ok, value}
else
{:error, message} -> {:error, {message, path: uri.path}}
end
end

defp fetch_file(path) do
case File.read(path) do
{:ok, content} -> {:ok, content}
{:error, reason} -> {:error, "#{reason}"}
end
end

defp decode_json(content) do
case Jason.decode(content) do
{:ok, value} -> {:ok, value}
{:error, reason} -> {:error, Exception.message(reason)}
end
end
end
15 changes: 14 additions & 1 deletion lib/resource_kit/json_pointer/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ defmodule ResourceKit.JSONPointer.Utils do
def resolve(parent, [], ctx), do: {:ok, parent, ctx}

def resolve(parent, [key | rest], ctx) when is_map(parent) do
case Map.fetch(parent, key) do
parent
|> stringify_keys()
|> Map.fetch(key)
|> case do
{:ok, data} -> resolve(data, rest, push_token(ctx, key))
:error -> {:error, {@key_not_exists, location: encode_location(ctx), key: key}}
end
Expand All @@ -43,6 +46,16 @@ defmodule ResourceKit.JSONPointer.Utils do
end
end

defp stringify_keys(data) when is_struct(data) do
data
|> Map.from_struct()
|> stringify_keys()
end

defp stringify_keys(data) do
JetExt.Map.stringify_keys(data)
end

# leading zeros are not allowed for index
defp parse_index(<<?0, _x, _rest::binary>> = index) do
{:error, {@index_has_leading_zeros, index: index}}
Expand Down
19 changes: 10 additions & 9 deletions lib/resource_kit/pipeline/compile/deref.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule ResourceKit.Pipeline.Compile.Deref do

import ResourceKit.Guards

alias ResourceKit.Deref.Context, as: DerefContext
alias ResourceKit.Pipeline.Compile.Token
alias ResourceKit.Schema.Ref

Expand All @@ -24,33 +25,33 @@ defmodule ResourceKit.Pipeline.Compile.Deref do
@impl Pluggable
def call(%Token{halted: true} = token, _opts), do: token

def call(%Token{action: action} = token, _opts) do
with {:ok, action} <- deref_schema(action),
{:ok, action} <- deref_returning_schema(action) do
def call(%Token{action: action, context: ctx} = token, _opts) do
with {:ok, action} <- deref_schema(action, ctx),
{:ok, action} <- deref_returning_schema(action, ctx) do
Token.put_assign(token, :action, action)
else
{:error, reason} ->
Token.put_error(token, reason)
end
end

defp deref_schema(%{"schema" => ref} = action) when is_ref(ref) do
defp deref_schema(%{"schema" => ref} = action, ctx) when is_ref(ref) do
with {:ok, ref} <- cast_ref(ref),
{:ok, schema} <- ResourceKit.Utils.deref(ref) do
{:ok, schema} <- ResourceKit.Deref.fetch(ref, %DerefContext{current: ctx.current}) do
{:ok, Map.put(action, "schema", schema)}
end
end

defp deref_schema(action), do: {:ok, action}
defp deref_schema(action, _ctx), do: {:ok, action}

defp deref_returning_schema(%{"returning_schema" => ref} = action) when is_ref(ref) do
defp deref_returning_schema(%{"returning_schema" => ref} = action, ctx) when is_ref(ref) do
with {:ok, ref} <- cast_ref(ref),
{:ok, returning} <- ResourceKit.Utils.deref(ref) do
{:ok, returning} <- ResourceKit.Deref.fetch(ref, %DerefContext{current: ctx.current}) do
{:ok, Map.put(action, "returning_schema", returning)}
end
end

defp deref_returning_schema(action), do: {:ok, action}
defp deref_returning_schema(action, _ctx), do: {:ok, action}

defp cast_ref(ref) do
ref |> Ref.changeset() |> Ecto.Changeset.apply_action(:insert)
Expand Down
37 changes: 23 additions & 14 deletions lib/resource_kit/pipeline/compile/preload_reference.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule ResourceKit.Pipeline.Compile.PreloadReference do

@behaviour Pluggable

alias ResourceKit.Deref.Context, as: DerefContext
alias ResourceKit.Pipeline.Compile.Token
alias ResourceKit.Schema.Column
alias ResourceKit.Schema.Ref
Expand All @@ -21,10 +22,10 @@ defmodule ResourceKit.Pipeline.Compile.PreloadReference do
@impl Pluggable
def call(%Token{halted: true} = token, _opts), do: token

def call(%Token{} = token, _opts) do
def call(%Token{context: context} = token, _opts) do
action = Token.fetch_assign!(token, :action)

case preload_schema(action.schema) do
case preload_schema(action.schema, context) do
{:ok, schema, references} ->
token
|> Token.put_assign(:action, %{action | schema: schema})
Expand All @@ -35,10 +36,10 @@ defmodule ResourceKit.Pipeline.Compile.PreloadReference do
end
end

defp preload_schema(%Schema{} = schema, references \\ %{}) do
defp preload_schema(%Schema{} = schema, context, references \\ %{}) do
schema.columns
|> Enum.reduce_while({:ok, [], references}, fn column, {:ok, columns, references} ->
case preload_column(column, references) do
case preload_column(column, context, references) do
{:ok, column, references} -> {:cont, {:ok, [column | columns], references}}
{:error, reason} -> {:halt, {:error, reason}}
end
Expand All @@ -49,31 +50,39 @@ defmodule ResourceKit.Pipeline.Compile.PreloadReference do
end
end

defp preload_column(column, references)
defp preload_column(column, context, references)

defp preload_column(%Column.Literal{} = column, references), do: {:ok, column, references}
defp preload_column(%Column.Literal{} = column, _context, references),
do: {:ok, column, references}

defp preload_column(column, references)
defp preload_column(column, context, references)
when is_struct(column, Column.Belongs) or is_struct(column, Column.Has) do
with {:ok, schema, references} <-
resolve_association_schema(column.association_schema, references),
{:ok, _schema, references} <- preload_schema(schema, references) do
resolve_association_schema(column.association_schema, context, references),
{:ok, _schema, references} <-
preload_schema(schema, update_context(column.association_schema, context), references) do
{:ok, column, references}
end
end

defp resolve_association_schema(%Ref{} = ref, references) do
case resolve(ref, references) do
defp resolve_association_schema(%Ref{} = ref, context, references) do
case resolve(ref, context, references) do
{:ok, schema} -> {:ok, schema, Map.put_new(references, ref, schema)}
{:error, reason} -> {:error, reason}
end
end

defp resolve_association_schema(schema, references), do: {:ok, schema, references}
defp resolve_association_schema(schema, _context, references), do: {:ok, schema, references}

defp resolve(ref, references) do
defp update_context(%Ref{uri: uri}, context) do
%{context | current: uri}
end

defp update_context(_schema, context), do: context

defp resolve(ref, context, references) do
with :error <- Map.fetch(references, ref) do
ResourceKit.Utils.resolve_association_schema(ref)
ResourceKit.Utils.resolve_association_schema(ref, %DerefContext{current: context.current})
end
end
end
7 changes: 7 additions & 0 deletions lib/resource_kit/pipeline/compile/token.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
defmodule ResourceKit.Pipeline.Compile.Token do
@moduledoc false

use TypedStruct
use ResourceKit.Pipeline.Token

typedstruct module: Context do
field :root, URI.t(), enforce: true
field :current, URI.t(), enforce: true
end

token do
field :action, map(), enforce: true
field :context, Context.t(), enforce: true
end
end
Loading

0 comments on commit 7e795e9

Please sign in to comment.