Skip to content

Commit

Permalink
Merge pull request #140 from bluzky/feature/define-params
Browse files Browse the repository at this point in the history
Feature/define params
  • Loading branch information
bluzky authored Feb 25, 2024
2 parents 15e9248 + f3aafdf commit 6d9aeee
Show file tree
Hide file tree
Showing 20 changed files with 539 additions and 31 deletions.
6 changes: 6 additions & 0 deletions lib/orange_cms.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ defmodule OrangeCms do
end
end

def param do
quote do
use OrangeCms.Params
end
end

@doc """
When used, dispatch to the appropriate context/schema/service/repo/finder
"""
Expand Down
6 changes: 4 additions & 2 deletions lib/orange_cms/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule OrangeCms.Projects do
@moduledoc false
use OrangeCms, :context

alias OrangeCms.Context
alias OrangeCms.Projects.Project
alias OrangeCms.Projects.ProjectMember

Expand Down Expand Up @@ -46,8 +47,9 @@ defmodule OrangeCms.Projects do
{:error, %Ecto.Changeset{}}
"""
def create_project(attrs, creator) do
OrangeCms.Projects.CreateProjectUsecase.call(attrs, creator)
@spec create_project(map, Context.t()) :: {:ok, Project.t()} | {:error, Ecto.Changeset.t()}
def create_project(attrs, context) do
OrangeCms.Projects.CreateProjectUsecase.call(attrs, context)
end

@doc """
Expand Down
11 changes: 8 additions & 3 deletions lib/orange_cms/projects/commands/create_project_command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ defmodule OrangeCms.Projects.CreateProjectCommand do
"""
use OrangeCms, :command

alias OrangeCms.Projects.CreateProjectParams
alias OrangeCms.Projects.Project
alias OrangeCms.Value

def call(attrs, creator) do
def call(%CreateProjectParams{} = params, creator) do
# add default member
attrs = Map.put(attrs, :project_members, [%{user_id: creator.id, role: :admin, is_owner: true}])
params =
params
|> Value.new()
|> Map.put(:project_members, [%{user_id: creator.id, role: :admin, is_owner: true}])

%Project{owner_id: creator.id}
|> Project.changeset(attrs)
|> Project.changeset(params)
|> Project.change_members()
|> Repo.insert()
end
Expand Down
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions lib/orange_cms/projects/params/create_project_params.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule OrangeCms.Projects.CreateProjectParams do
@moduledoc false
use OrangeCms.Params

defparams do
field :name, :string, required: true
field :type, :any, default: "github", in: ["github", "headless_cms", :github, :headless_cms]
end
end
15 changes: 8 additions & 7 deletions lib/orange_cms/projects/usecases/create_project_usecase.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ defmodule OrangeCms.Projects.CreateProjectUsecase do
Create a project with given params
Add creator as the project owner
"""
alias OrangeCms.Projects.CreateProjectParams

def call(attrs, actor) do
attrs
|> OrangeCms.Projects.CreateProjectCommand.call(actor)
|> handle_result()
def call(params, %{actor: actor} = _context) do
with {:ok, parsed_params} <- CreateProjectParams.cast(params),
{:ok, project} <- OrangeCms.Projects.CreateProjectCommand.call(parsed_params, actor) do
{:ok, project}
else
{:error, error} -> {:error, error}
end
end

defp handle_result({:error, changeset}), do: {:error, changeset}
defp handle_result({:ok, project}), do: {:ok, project}
end
51 changes: 51 additions & 0 deletions lib/orange_cms/shared/context.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule OrangeCms.Context do
@moduledoc """
This module is the context for the OrangeCms application
A context is a struct which hold commonly data that business logic needs and should aware of.
Details of the context:
- actor: The user who is performing the action
- project: The project that the action is performed on
- resource: The resource that the action is performed on
- extra: Extra data that the action needs
"""
defstruct [:actor, :project, :resource, extra: %{}]

@doc """
Create a new empty context
"""
@spec new() :: %__MODULE__{}
def new do
%__MODULE__{}
end

@doc """
Create a new context with given actor, project, resource and extra data
"""
@spec new(actor :: any, project :: any, resource :: any, extra :: map) :: %__MODULE__{}
def new(actor, project, resource, extra \\ %{}) do
%__MODULE__{actor: actor, project: project, resource: resource, extra: extra}
end

@doc """
Create new context with data provided in a map or keyword list
"""
@spec new(attrs :: map) :: %__MODULE__{}
def new(attrs) when is_map(attrs) or is_list(attrs) do
struct(__MODULE__, attrs)
end

@doc """
Get the value of a key from the context. Only accepts atoms as keys
## Examples
iex> context = OrangeCms.Context.new(actor: use, project: project)
iex> OrangeCms.Context.put(context, :resource, post)
"""
@spec put(context :: %__MODULE__{}, key :: atom, value :: any) :: %__MODULE__{}
def put(context, key, value) do
struct(context, [{key, value}])
end
end
187 changes: 187 additions & 0 deletions lib/orange_cms/shared/def_params.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
defmodule OrangeCms.Params do
@moduledoc """
Borrow from https://github.com/ejpcmac/typed_struct/blob/main/lib/typed_struct.ex
"""
@accumulating_attrs [
:ts_fields,
:ts_types,
:ts_enforce_keys
]

@attrs_to_delete @accumulating_attrs

@doc false
defmacro __using__(_) do
quote do
import OrangeCms.Params, only: [defparams: 1, defparams: 2]
end
end

@doc """
Defines a typed struct.
Inside a `defparams` block, each field is defined through the `field/2`
macro.
## Examples
defmodule MyStruct do
use OrangeCms.Params
defparams do
field :field_one, :string
field :field_two, :integer, required: true
field :field_three, :boolean, required: true
field :field_four, :atom, default: :hey
field :update_time, :naive_datetime, default: &NaiveDateTime.utc_now/0
end
end
You can create the struct in a submodule instead:
defmodule MyModule do
use OrangeCms.Params
defparams Comment do
field :user_id, :integer, required: true
field :content, :string, required: true
end
defparams Post do
field :field_one, :string
field :field_two, :integer, required: true
field :field_three, :boolean, required: true
field :field_four, :string, default: "hello"
field :update_time, :naive_datetime, default: &NaiveDateTime.utc_now/0
field :comment, Comment, required: true
end
end
MyModule.Post.cast(%{field_two: 1, field_three: true, comment: %{user_id: 1, content: "hello"}})
"""
defmacro defparams(module \\ nil, do: block) do
ast = OrangeCms.Params.__typedstruct__(block)
method_ast = OrangeCms.Params.__default_functions__()

case module do
nil ->
quote do
# Create a lexical scope.
(fn -> unquote(ast) end).()
unquote(method_ast)
end

module ->
quote do
defmodule unquote(module) do
unquote(ast)

unquote(method_ast)
end
end
end
end

def __default_functions__ do
quote do
def new(struct) when is_struct(struct) do
struct(__MODULE__, Map.from_struct(struct))
end

def new(map) do
struct(__MODULE__, map)
end

def cast(params) when is_map(params) do
case Tarams.cast(params, @ts_fields) do
{:ok, params} -> {:ok, new(params)}
{:error, errors} -> {:error, %OrangeCms.ParamsError{errors: errors, data: params}}
end
end

def __schema__(:fields) do
@ts_fields
end
end
end

@doc false
def __typedstruct__(block) do
quote do
import OrangeCms.Params

Enum.each(unquote(@accumulating_attrs), fn attr ->
Module.register_attribute(__MODULE__, attr, accumulate: true)
end)

@before_compile {unquote(__MODULE__), :__before_compile__}
unquote(block)

@enforce_keys @ts_enforce_keys
defstruct @ts_fields

OrangeCms.Params.__type__(@ts_types)
end
end

@doc false
defmacro __type__(types) do
quote bind_quoted: [types: types] do
@type t() :: %__MODULE__{unquote_splicing(types)}
end
end

@doc """
Defines a field in a typed struct.
## Example
# A field named :example of type String.t()
field :example, String.t()
## Options
* `default` - sets the default value for the field
* `enforce` - if set to true, enforces the field and makes its type
non-nullable
"""
defmacro field(name, type, opts \\ []) do
quote bind_quoted: [name: name, type: type, opts: opts] do
OrangeCms.Params.__field__(name, type, opts, __ENV__)
end
end

@doc false
def __field__(name, type, opts, %Macro.Env{module: mod}) when is_atom(name) do
if mod |> Module.get_attribute(:ts_fields) |> Keyword.has_key?(name) do
raise ArgumentError, "the field #{inspect(name)} is already set"
end

has_default? = Keyword.has_key?(opts, :default)

enforce? =
if is_nil(opts[:required]),
do: not has_default?,
else: opts[:required] == true

nullable? = not has_default? and not enforce?

Module.put_attribute(mod, :ts_fields, {name, [{:type, type} | opts]})
Module.put_attribute(mod, :ts_types, {name, type_for(type, nullable?)})
if enforce?, do: Module.put_attribute(mod, :ts_enforce_keys, name)
end

def __field__(name, _type, _opts, _env) do
raise ArgumentError, "a field name must be an atom, got #{inspect(name)}"
end

# Makes the type nullable if the key is not enforced.
defp type_for(type, false), do: type
defp type_for(type, _), do: quote(do: unquote(type) | nil)

@doc false
defmacro __before_compile__(%Macro.Env{module: module}) do
Enum.each(unquote(@attrs_to_delete), &Module.delete_attribute(module, &1))
end
end
Loading

0 comments on commit 6d9aeee

Please sign in to comment.