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

[JET-2027] deref 抽象 #6

Merged
merged 7 commits into from
Oct 16, 2024
Merged
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
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