Skip to content

Commit a771ba2

Browse files
Update docs (#15)
1 parent 25aa624 commit a771ba2

File tree

5 files changed

+335
-7
lines changed

5 files changed

+335
-7
lines changed

.formatter.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Used by "mix format"
22
[
3-
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
4+
locals_without_parens: [resource: 2]
45
]

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ EctoResource
2020

2121
Eliminate boilerplate involved in defining basic CRUD functions in a Phoenix context or Elixir module.
2222

23-
About
23+
Rationale
2424
-----
2525
When using [Context modules](https://hexdocs.pm/phoenix/contexts.html) in a [Phoenix](https://phoenixframework.org/) application, there's a general need to define the standard CRUD functions for a given `Ecto.Schema`. Phoenix context generators will even do this automatically. Soon you will notice that there's quite a lot of code involved in CRUD access within your contexts.
2626

lib/ecto_resource.ex

+329-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,334 @@
11
defmodule EctoResource do
22
@moduledoc """
3-
This module provides a DSL to easily generate the basic functions for a schema.
4-
This allows the context to focus on interesting, atypical implementations rather
5-
than the redundent, drifting CRUD functions.
3+
EctoResource
4+
============
5+
Eliminate boilerplate involved in defining basic CRUD functions in a Phoenix context or Elixir module.
6+
7+
When using [Context modules](https://hexdocs.pm/phoenix/contexts.html) in a [Phoenix](https://phoenixframework.org/) application,
8+
there's a general need to define the standard CRUD functions for a given `Ecto.Schema`. Phoenix context generators will even do this automatically.
9+
Soon you will notice that there's quite a lot of code involved in CRUD access within your contexts.
10+
11+
This can become problematic for a few reasons:
12+
13+
* Boilerplate functions for CRUD access, for every `Ecto.Schema` referenced in that context, introduce more noise than signal. This can obscure the more interesting details of the context.
14+
* These functions may tend to accumulate drift from the standard API by inviting edits for new use-cases, reducing the usefulness of naming conventions.
15+
* The burden of locally testing wrapper functions, yields low value for the writing and maintainence investment.
16+
17+
In short, at best this code is redundant and at worst is a deviant entanglement of modified conventions. All of which amounts to a more-painful development experience. `EctoResource` was created to ease this pain.
18+
19+
Usage
20+
-----
21+
22+
### Basic usage - generate all `EctoResource` functions
23+
24+
```elixir
25+
defmodule MyApp.MyContext do
26+
alias MyApp.Repo
27+
alias MyApp.Schema
28+
use EctoResource
29+
30+
using_repo(Repo) do
31+
resource(Schema)
32+
end
33+
end
34+
```
35+
36+
This generates all the functions `EctoResource` has to offer:
37+
38+
* `MyContext.all_schemas/1`
39+
* `MyContext.change_schema/1`
40+
* `MyContext.create_schema/1`
41+
* `MyContext.create_schema!/1`
42+
* `MyContext.delete_schema/1`
43+
* `MyContext.delete_schema!/1`
44+
* `MyContext.get_schema/2`
45+
* `MyContext.get_schema!/2`
46+
* `MyContext.get_schema_by/2`
47+
* `MyContext.get_schema_by!/2`
48+
* `MyContext.update_schema/2`
49+
* `MyContext.update_schema!/2`
50+
51+
### Explicit usage - generate only given functions
52+
53+
```elixir
54+
defmodule MyApp.MyContext do
55+
alias MyApp.Repo
56+
alias MyApp.Schema
57+
use EctoResource
58+
59+
using_repo(Repo) do
60+
resource(Schema, only: [:create, :delete!])
61+
end
62+
end
63+
```
64+
65+
This generates only the given functions:
66+
67+
* `MyContext.create_schema/1`
68+
* `MyContext.delete_schema!/1`
69+
70+
### Exclusive usage - generate all but the given functions
71+
72+
```elixir
73+
defmodule MyApp.MyContext do
74+
alias MyApp.Repo
75+
alias MyApp.Schema
76+
use EctoResource
77+
78+
using_repo(Repo) do
79+
resource(Schema, except: [:create, :delete!])
80+
end
81+
end
82+
```
83+
84+
This generates all the functions excluding the given functions:
85+
86+
* `MyContext.all_schemas/1`
87+
* `MyContext.change_schema/1`
88+
* `MyContext.create_schema!/1`
89+
* `MyContext.delete_schema/1`
90+
* `MyContext.get_schema/2`
91+
* `MyContext.get_schema_by/2`
92+
* `MyContext.get_schema_by!/2`
93+
* `MyContext.get_schema!/2`
94+
* `MyContext.update_schema/2`
95+
* `MyContext.update_schema!/2`
96+
97+
### Alias `:read` - generate data access functions
98+
99+
```elixir
100+
defmodule MyApp.MyContext do
101+
alias MyApp.Repo
102+
alias MyApp.Schema
103+
use EctoResource
104+
105+
using_repo(Repo) do
106+
resource(Schema, :read)
107+
end
108+
end
109+
```
110+
111+
This generates all the functions necessary for reading data:
112+
113+
* `MyContext.all_schemas/1`
114+
* `MyContext.get_schema/2`
115+
* `MyContext.get_schema!/2`
116+
117+
### Alias `:read_write` - generate data access and manipulation functions, excluding delete
118+
119+
```elixir
120+
defmodule MyApp.MyContext do
121+
alias MyApp.Repo
122+
alias MyApp.Schema
123+
use EctoResource
124+
125+
using_repo(Repo) do
126+
resource(Schema, :read_write)
127+
end
128+
end
129+
```
130+
131+
This generates all the functions except `delete_schema/1` and `delete_schema!/1`:
132+
133+
* `MyContext.all_schemas/1`
134+
* `MyContext.change_schema/1`
135+
* `MyContext.create_schema/1`
136+
* `MyContext.create_schema!/1`
137+
* `MyContext.get_schema/2`
138+
* `MyContext.get_schema!/2`
139+
* `MyContext.update_schema/2`
140+
* `MyContext.update_schema!/2`
141+
142+
### Resource functions
143+
144+
The general idea of the generated resource functions is to abstract away the `Ecto.Repo` and `Ecto.Schema` parts of data access with `Ecto` and provide an API to the context that feels natural and clear to the caller.
145+
146+
The following examples will all assume a repo named `Repo` and a schema named `Person`.
147+
148+
#### all_people
149+
150+
Fetches a list of all %Person{} entries from the data store. _Note: `EctoResource` will pluralize this function name using `Inflex`_
151+
152+
```elixir
153+
iex> all_people()
154+
[%Person{id: 1}]
155+
156+
iex> all_people(preloads: [:address])
157+
[%Person{id: 1, address: %Address{}}]
158+
159+
iex> all_people(order_by: [desc: :id])
160+
[%Person{id: 2}, %Person{id: 1}]
161+
162+
iex> all_people(preloads: [:address], order_by: [desc: :id]))
163+
[
164+
%Person{
165+
id: 2,
166+
address: %Address{}
167+
},
168+
%Person{
169+
id: 1,
170+
address: %Address{}
171+
}
172+
]
173+
174+
iex> all_people(where: [id: 2])
175+
[%Person{id: 2, address: %Address{}}]
176+
```
177+
178+
#### change_person
179+
180+
Creates a `%Person{}` changeset.
181+
182+
```elixir
183+
iex> change_person(%{name: "Example Person"})
184+
#Ecto.Changeset<
185+
action: nil,
186+
changes: %{name: "Example Person"},
187+
errors: [],
188+
data: #Person<>,
189+
valid?: true
190+
>
191+
```
192+
193+
#### create_person
194+
195+
Inserts a `%Person{}` with the given attributes in the data store, returning an `:ok`/`:error` tuple.
196+
197+
```elixir
198+
iex> create_person(%{name: "Example Person"})
199+
{:ok, %Person{id: 123, name: "Example Person"}}
200+
201+
iex> create_person(%{invalid: "invalid"})
202+
{:error, %Ecto.Changeset}
203+
```
204+
205+
#### create_person!
206+
207+
Inserts a `%Person{}` with the given attributes in the data store, returning a `%Person{}` or raises `Ecto.InvalidChangesetError`.
208+
209+
```elixir
210+
iex> create_person!(%{name: "Example Person"})
211+
%Person{id: 123, name: "Example Person"}
212+
213+
iex> create_person!(%{invalid: "invalid"})
214+
** (Ecto.InvalidChangesetError)
215+
```
216+
217+
#### delete_person
218+
219+
Deletes a given `%Person{}` from the data store, returning an `:ok`/`:error` tuple.
220+
221+
```elixir
222+
iex> delete_person(%Person{id: 1})
223+
{:ok, %Person{id: 1}}
224+
225+
iex> delete_person(%Person{id: 999})
226+
{:error, %Ecto.Changeset}
227+
```
228+
229+
#### delete_person!
230+
231+
Deletes a given `%Person{}` from the data store, returning the deleted `%Person{}`, or raises `Ecto.StaleEntryError`.
232+
233+
```elixir
234+
iex> delete_person!(%Person{id: 1})
235+
%Person{id: 1}
236+
237+
iex> delete_person!(%Person{id: 999})
238+
** (Ecto.StaleEntryError)
239+
```
240+
241+
#### get_person
242+
243+
Fetches a single `%Person{}` from the data store where the primary key matches the given id, returns a `%Person{}` or `nil`.
244+
245+
```elixir
246+
iex> get_person(1)
247+
%Person{id: 1}
248+
249+
iex> get_person(999)
250+
nil
251+
252+
iex> get_person(1, preloads: [:address])
253+
%Person{
254+
id: 1,
255+
address: %Address{}
256+
}
257+
```
258+
259+
#### get_person!
260+
261+
Fetches a single `%Person{}` from the data store where the primary key matches the given id, returns a `%Person{}` or raises `Ecto.NoResultsError`.
262+
263+
```elixir
264+
iex> get_person!(1)
265+
%Person{id: 1}
266+
267+
iex> get_person!(999)
268+
** (Ecto.NoResultsError)
269+
270+
iex> get_person!(1, preloads: [:address])
271+
%Person{
272+
id: 1,
273+
address: %Address{}
274+
}
275+
```
276+
277+
#### get_person_by
278+
279+
Fetches a single `%Person{}` from the data store where the attributes match the
280+
given values.
281+
282+
```elixir
283+
iex> get_person_by(%{name: "Chuck Norris"})
284+
%Person{name: "Chuck Norris"}
285+
286+
iex> get_person_by(%{name: "Doesn't Exist"})
287+
nil
288+
```
289+
290+
#### get_person_by!
291+
292+
Fetches a single `%Person{}` from the data store where the attributes match the
293+
given values. Raises an `Ecto.NoResultsError` if the record does not exist
294+
295+
```elixir
296+
iex> get_person_by!(%{name: "Chuck Norris"})
297+
%Person{name: "Chuck Norris"}
298+
299+
iex> get_person_by!(%{name: "Doesn't Exist"})
300+
** (Ecto.NoResultsError)
301+
```
302+
303+
#### update_person
304+
305+
Updates a given %Person{} with the given attributes, returns an `:ok`/`:error` tuple.
306+
307+
```elixir
308+
iex> update_person(%Person{id: 1}, %{name: "New Person"})
309+
{:ok, %Person{id: 1, name: "New Person"}}
310+
311+
iex> update_person(%Person{id: 1}, %{invalid: "invalid"})
312+
{:error, %Ecto.Changeset}
313+
```
314+
315+
#### update_person!
316+
317+
Updates a given %Person{} with the given attributes, returns a %Person{} or raises `Ecto.InvalidChangesetError`.
318+
319+
```elixir
320+
iex> update_person!(%Person{id: 1}, %{name: "New Person"})
321+
%Person{id: 1, name: "New Person"}
322+
323+
iex> update_person!(%Person{id: 1}, %{invalid: "invalid"})
324+
** (Ecto.InvalidChangesetError)
325+
```
326+
327+
Caveats
328+
-------
329+
This is not meant to be used as a wrapper for all the Repo functions within a context. Not all callbacks defined in Ecto.Repo are generated. `EctoResource` should be used to help reduce boilerplate code and tests for general CRUD operations.
330+
331+
It may be the case that `EctoResource` needs to evolve and provide slightly more functionality/flexibility in the future. However, the general focus is reducing boilerplate code.
6332
"""
7333

8334
alias __MODULE__

lib/test_repo.ex

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
defmodule EctoResource.TestRepo do
2+
@moduledoc false
23
use Ecto.Repo,
34
otp_app: :ecto_resource,
45
adapter: Ecto.Adapters.Postgres

mix.exs

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ defmodule EctoResource.MixProject do
4949
defp package do
5050
[
5151
files: ["lib", "mix.exs", "README*", "LICENSE*"],
52-
maintainers: ["Dayton Nolan"],
5352
licenses: ["Apache 2.0"],
54-
links: %{"GitHub" => "https://github.com/testdouble/ecto_resource"}
53+
links: %{"GitHub" => "https://github.com/testdouble/ecto_resource"},
54+
source_url: "https://github.com/testdouble/ecto_resource"
5555
]
5656
end
5757

0 commit comments

Comments
 (0)