Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
defmodule ElixirApiWeb.BlockedTimesController do
@moduledoc false

use ElixirApiWeb, :controller

action_fallback(ElixirApiWeb.FallbackController)

# add aliases here

def index(conn, params) do
with {:ok, date_from, date_to, location_id, employee_ids} <- parse_index_params(params) do
# TODO this is the part you need to implement by yourself
# TODO evaluate extra arguments, then pass them to render()
render(conn)
else
{:error, :invalid_parameters, params} ->
{:error, :invalid_parameters, params}

{:error, :invalid_pointers, pointers} ->
{:error, :invalid_parameters, pointers}

{:error, :not_found} ->
{:error, :not_found}
end
end

defp parse_index_params(params) do
flat_parse(
params,
date_from: [:date, :required],
date_to: :date,
location_id: [:string, :required],
employee_ids: :id_list,
)
end

def create(conn, _params) do
with {:ok, parsed_opts} <- parse_create_conn(conn) do
# TODO this is the part you need to implement by yourself
# TODO evaluate extra arguments, then pass them to render()
render(conn)
else
{:error, :invalid_parameters, params} ->
{:error, :invalid_parameters, params}

{:error, :invalid_pointers, pointers} ->
{:error, :invalid_parameters, pointers}
end
end

defp parse_create_conn(conn) do
parse(
conn.assigns[:doc],
id: [:id, :required],
attributes: %{
start: [&naive_date_time/1, :required],
end: [&naive_date_time/1, :required],
note: :string,
is_private: :boolean,
},
relationships: %{
employee: [:resource_id, :required],
location: [:resource_id, :required],
},
)
end

def update(conn, params) do
with {:ok, id} <- parse_update_params(params),
{:ok, parsed_opts} <- parse_update_conn(conn) do
# TODO this is the part you need to implement by yourself
# TODO evaluate extra arguments, then pass them to render()
render(conn)
else
{:error, :invalid_parameters, params} ->
{:error, :invalid_parameters, params}

{:error, :invalid_pointers, pointers} ->
{:error, :invalid_parameters, pointers}

{:error, :not_found} ->
{:error, :not_found}
end
end

defp parse_update_params(params) do
flat_parse(
params,
id: [:id, :required],
)
end

defp parse_update_conn(conn) do
parse(
conn.assigns[:doc],
id: [:id, :required],
attributes: %{
start: [&naive_date_time/1, :required],
end: [&naive_date_time/1, :required],
note: :string,
is_private: :boolean,
},
relationships: %{
employee: :resource_id,
},
)
end

def delete(conn, params) do
with {:ok, id} <- parse_delete_params(params) do
# TODO this is the part you need to implement by yourself
# TODO evaluate extra arguments, then pass them to render()
render(conn)
else
{:error, :invalid_parameters, params} ->
{:error, :invalid_parameters, params}

{:error, :invalid_pointers, pointers} ->
{:error, :invalid_parameters, pointers}
end
end

defp parse_delete_params(params) do
flat_parse(
params,
id: [:id, :required],
)
end

defp naive_date_time(nil), do: {:error, :invalid_date_time}

defp naive_date_time(input) do
case NaiveDateTime.from_iso8601(input) do
{:ok, date_time} -> {:ok, date_time}
{:error, _} -> {:error, :invalid_date_time}
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule ElixirApiWeb.BlockedTimesExportController do
@moduledoc false

use ElixirApiWeb, :controller

action_fallback(ElixirApiWeb.FallbackController)

# add aliases here

def create(conn, _params) do
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a great start!

One obvious issue I'm seeing is that after the developer implements the missing parts they can't regenerate the thing without undoing their work.

The answer to this is ofc better modularity

We should make the thing modular enough that The controller code is actually dumb enough to not need any manual changes ever. That would require us to agree on some conventions as to what the code in the controller action should look like and what should it call into and what the error handling should look like.

same thing is obiously true about the views, the tests, the router, etc.
There are many ways to achieve this separation between generated and nongenerated code.
It's not ease to think of optimal patterns for this from the top of my head.

Another thing that comes to my mind when trying to think of the patterns is that
if we make the generator smart enough to know that If an action/test/view already exists, then It should not regenerate it then it would be much easier to make the separation natural and not requiring splitting things into many files. Wdyt? I know it's not easy to make a foolproof mechanism like that without actually parsing the code (or using mysterious nonstandard extended regexes), so it might not be worth to introduce this complexity.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to get the most value from the spec->code workflow then we have to enable painfree, nondestructive generation. I'm stating the obvious here - generation of a new endpoint should not destroy a whole controller, etc

with {:ok, parsed_opts} <- parse_create_conn(conn) do
# TODO this is the part you need to implement by yourself
# TODO evaluate extra arguments, then pass them to render()
render(conn)
else
{:error, :invalid_parameters, params} ->
{:error, :invalid_parameters, params}

{:error, :invalid_pointers, pointers} ->
{:error, :invalid_parameters, pointers}
end
end

defp parse_create_conn(conn) do
parse(
conn.assigns[:doc],
id: [:id, :required],
attributes: %{
start: [&naive_date_time/1, :required],
end: [&naive_date_time/1, :required],
},
relationships: %{
},
)
end

def show(conn, params) do
with {:ok, id} <- parse_show_params(params) do
# TODO this is the part you need to implement by yourself
# TODO evaluate extra arguments, then pass them to render()
render(conn)
else
{:error, :invalid_parameters, params} ->
{:error, :invalid_parameters, params}

{:error, :invalid_pointers, pointers} ->
{:error, :invalid_parameters, pointers}

{:error, :not_found} ->
{:error, :not_found}
end
end

defp parse_show_params(params) do
flat_parse(
params,
id: [:id, :required],
)
end

defp naive_date_time(nil), do: {:error, :invalid_date_time}

defp naive_date_time(input) do
case NaiveDateTime.from_iso8601(input) do
{:ok, date_time} -> {:ok, date_time}
{:error, _} -> {:error, :invalid_date_time}
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
defmodule ElixirApiWeb.FallbackController do
@moduledoc false

use ElixirApiWeb, :controller
alias ElixirApiWeb.ErrorView

def call(conn, {:error, :bad_request}) do
conn
|> put_status(:bad_request)
|> put_view(ErrorView)
|> render(:"400")
end

def call(conn, {:error, :bad_request, code}) when is_atom(code) do
conn
|> put_status(:bad_request)
|> put_view(ErrorView)
|> render(:"400", code: Atom.to_string(code))
end

def call(conn, {:error, :invalid_parameters, params}) when is_list(params) do
conn
|> put_status(:bad_request)
|> put_view(ErrorView)
|> render(:"400", params: params)
end

def call(conn, {:error, :invalid_pointers, pointers}) when is_list(pointers) do
conn
|> put_status(:bad_request)
|> put_view(ErrorView)
|> render(:"400", pointers: pointers)
end

def call(conn, {:error, :unauthorized}) do
conn
|> put_status(:unauthorized)
|> put_view(ErrorView)
|> render(:"401")
end

def call(conn, {:error, :forbidden}) do
conn
|> put_status(:forbidden)
|> put_view(ErrorView)
|> render(:"403")
end

def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(ErrorView)
|> render(:"404")
end

def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(ErrorView)
|> render(:"422", changeset: changeset)
end

def call(conn, {:error, :validation_failed}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(ErrorView)
|> render(:"422")
end

def call(conn, {:error, :unprocessable_entity}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(ErrorView)
|> render(:"422")
end

def call(conn, {:error, :conflict}) do
conn
|> put_status(:conflict)
|> put_view(ErrorView)
|> render(:"409")
end

def call(conn, {:error, :not_available}) do
conn
|> put_status(:service_unavailable)
|> put_view(ErrorView)
|> render(:"503")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule ElixirApiWeb.BlockedTimesResource do
@moduledoc false

@resource_type "blocked-times"
use Jabbax.Document
alias ElixirApiWeb.EmployeesResource
alias ElixirApiWeb.LocationsResource

def build(config) do
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to also generate typespecs for the resources

%Resource{
type: @resource_type,
id: config.id,
attributes: %{
start: config.start,
end: config.end,
note: config.note,
is_private: config.is_private,
},
relationships:
%{}
|> link_relationship(:employee, config.employee)
|> link_relationship(:location, config.location)
}
end

def link(config) do
%ResourceId{
type: @resource_type,
id: config.id,
}
end

defp link_relationship(relationships, type, nil) do
Map.put(relationships, type, %Jabbax.Document.Relationship{data: nil})
end

defp link_relationship(relationships, :employee, employee) do
Map.put(relationships, :employee, EmployeesResource.link(employee))
end

defp link_relationship(relationships, :location, location) do
Map.put(relationships, :location, LocationsResource.link(location))
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule ElixirApiWeb.EmployeesResource do
@moduledoc false

@resource_type "employees"
use Jabbax.Document

def link(config) do
%ResourceId{
type: @resource_type,
id: config.id,
}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule ElixirApiWeb.ExportJobsResource do
@moduledoc false

@resource_type "export-jobs"
use Jabbax.Document

def build(config) do
%Resource{
type: @resource_type,
id: config.id,
attributes: %{
status: config.status,
download_link: config.download_link,
},
}
end

def link(config) do
%ResourceId{
type: @resource_type,
id: config.id,
}
end
end
Loading