Skip to content

Commit

Permalink
Add docs to Dx.Defd module and functions
Browse files Browse the repository at this point in the history
  • Loading branch information
arnodirlam committed Oct 27, 2024
1 parent 06d6a71 commit 2c58cf0
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 6 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[![Last Updated](https://img.shields.io/github/last-commit/elixir-dx/dx/main)](https://github.com/elixir-dx/dx/tree/main)
![CI](https://github.com/elixir-dx/dx/actions/workflows/ci.yml/badge.svg)

<!-- MODULEDOC -->
Dx enables you to write Elixir code as if all your Ecto data is already (pre)loaded.

### Example
Expand Down Expand Up @@ -34,9 +35,12 @@ This can be called using
Dx.Defd.load!(MyApp.Core.Authorization.visible_lists(user))
```

All data needed is loaded automatically: The association `role`, and either all `Schema.List` matching the filter (translated to a single SQL query) or the user's associated lists, depending on whether it's an admin.
`Dx.Defd.load!/2` loads all required data automatically: The association `role`, and either all
`Schema.List` matching the filter (translated to a single SQL query) or the user's associated lists,
depending on whether it's an admin.

These function can just as well be called for many users, and Dx will load data efficiently (with batching and concurrently).
<!-- MODULEDOC -->

## Demo

Expand Down Expand Up @@ -76,6 +80,7 @@ Import the formatter rules in `.formatter.exs`:
]
```

<!-- MODULEDOC -->
## Background

Most server backends for web and mobile applications are split between
Expand Down Expand Up @@ -226,6 +231,7 @@ will be translated to database queries, if both
- `and`, `or`, `&&`
- `Enum.any?/2`, `Enum.all?/2`
- `DateTime.compare/2`
<!-- MODULEDOC -->

### Roadmap

Expand Down
3 changes: 2 additions & 1 deletion lib/dx.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Dx do
@moduledoc """
This is the main entry for using the Dx API.
This module used to be the main entry for using the Dx API. It is now deprecated.
Please use `Dx.Defd` instead.
- `get/3` evaluates the given predicate(s) using only the (pre)loaded data available, and returns the result(s)
- `load/3` is like `get`, but loads any additional data as needed
Expand Down
155 changes: 153 additions & 2 deletions lib/dx/defd.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
defmodule Dx.Defd do
@external_resource Path.expand("./README.md")
@moduledoc File.read!(Path.expand("./README.md"))
|> String.split("<!-- MODULEDOC -->")
|> Enum.drop(1)
|> Enum.take_every(2)
|> Enum.join()

alias Dx.Defd.Ast
alias Dx.Defd.Util
alias Dx.Evaluation, as: Eval

@eval_var Macro.var(:eval, Dx.Defd.Compiler)

@doc """
Like `load!/2` but returns `{:ok, result}` on success, `{:error, error}` on failure.
See `load!/2` for a raising alternative, and `get!/2` and `get/2` for non-loading alternatives.
"""
defmacro load(call, opts \\ []) do
defd_call = call_to_defd(call, __ENV__)

Expand All @@ -16,6 +28,29 @@ defmodule Dx.Defd do
|> mark_use(call)
end

@doc """
Wrap a `defd` function call to run it repeatedly, loading all required data.
Raises an error if unsuccessful.
See `load/2`, `get!/2` and `get/2` for non-raising and/or non-loading alternatives.
## Example
defmodule MyApp.Core.Authorization do
import Dx.Defd
defd visible_lists(user) do
if user.role.name == "Admin" do
Enum.filter(Schema.List, &(&1.title == "Main list"))
else
user.lists
end
end
end
# Will raise if data loading fails
Dx.Defd.load!(MyApp.Core.Authorization.visible_lists(user))
"""
defmacro load!(call, opts \\ []) do
defd_call = call_to_defd(call, __ENV__)

Expand All @@ -27,6 +62,16 @@ defmodule Dx.Defd do
|> mark_use(call)
end

@doc """
Like `load!/2` but returns a result tuple and evaluates without loading any data.
Returns either
- `{:ok, result}` on success
- `{:error, error}` on failure
- `{:not_loaded, data_reqs}` if required data is missing
See `get!/2` for a raising alternative, and `load!/2` and `load/2` for loading alternatives.
"""
defmacro get(call, opts \\ []) do
defd_call = call_to_defd(call, __ENV__)

Expand All @@ -38,6 +83,11 @@ defmodule Dx.Defd do
|> mark_use(call)
end

@doc """
Like `load!/2` but evaluates without loading any data.
See `get/2` for a non-raising alternative, and `load!/2` and `load/2` for loading alternatives.
"""
defmacro get!(call, opts \\ []) do
quote do
Dx.Defd.get(unquote(call), unquote(opts))
Expand Down Expand Up @@ -92,25 +142,126 @@ defmodule Dx.Defd do
end
end

@doc false
defmacro defd(call) do
define_defd(:def, call, __CALLER__)
end

@doc """
Defines a function that automatically loads required data.
`defd` functions are similar to regular Elixir functions defined with `def`,
but they allow you to write code as if all data is already loaded.
Dx will automatically handle loading the necessary data from the database
when the function is called.
## Usage
```elixir
defmodule MyApp.Core.Authorization do
import Dx.Defd
defd visible_lists(user) do
if admin?(user) do
Enum.filter(Schema.List, &(&1.title == "Main list"))
else
user.lists
end
end
defd admin?(user) do
user.role.name == "Admin"
end
end
```
This can be called using
```elixir
Dx.Defd.load!(MyApp.Core.Authorization.visible_lists(user))
```
## Important notes
- `defd` functions should be pure and not have side effects.
- They should not rely on external state or perform I/O operations.
- Calls to non-`defd` functions should be wrapped in `non_dx/1`.
- To call a `defd` function from regular Elixir code, wrap it in `Dx.Defd.load!/1`.
## Options
Additional options can be passed via a `@dx` attribute right before the `defd` definition:
```elixir
@dx def: :original
defd visible_lists(user) do
# ...
end
```
Available options:
- `def:` - Determines what the generated non-defd function should do.
- `:warn` (default) - Call the `defd` function wrapped in `Dx.Defd.load!/1` and emit a warning
asking to make the wrapping explicit.
- `:no_warn` - Call the `defd` function wrapped in `Dx.Defd.load!/1` without emitting a warning.
- `:original` - Keep the original function definition. This means, the original function can still
be called directly without being changed by Dx. The `defd` version *must* be called from other
`defd` functions or by wrapping the call in `Dx.Defd.load!/1`. This can be useful when migrating
existing code to Dx.
- `debug:` - Takes one or multiple flags for printing generated code to the console. These *can* get
*very* verbose, because Dx generates code for many combinations of cases. All flags have a `_raw`
variant that prints the code without syntax highlighting.
- `:original` - Prints the original function definition as passed to defd. All macros are already
expanded at this point.
- `:def` - Prints the `def` version, which is the generated non-defd function. See the `def:` option.
- `:defd` - Prints the `defd` function definition.
- `:final_args` - Prints the `final_args` version, which is similar to the `defd` version but
can be slightly shorter for some internal optimizations. This is also the version used
as the entrypoint when calling `Dx.Defd.load!/1`.
- `:scope` - Prints the `scope` version, which is used to translate the function to SQL.
It returns AST-like data structures with embedded `defd` code fallbacks.
- `:all` - Enables all the flags.
"""
defmacro defd(call, do: block) do
define_defd(:def, call, block, __CALLER__)
end

@doc false
defmacro defdp(call) do
define_defd(:defp, call, __CALLER__)
end

@doc "Private version of `defd/2`."
defmacro defdp(call, do: block) do
define_defd(:defp, call, block, __CALLER__)
end

@doc """
Used to wrap calls to non-Dx defined functions.
It doesn't run any code, but makes these calls explicit and mutes Dx compiler warnings.
Used to wrap calls to non-Dx defined functions within a `defd` function.
When writing `defd` functions, any calls to regular Elixir functions (non-`defd` functions)
should be wrapped with `non_dx/1`. This makes the external calls explicit and suppresses
Dx compiler warnings.
## Example
defmodule MyApp.Core.Stats do
import Dx.Defd
def calculate_percentage(value, total) do
(value / total) * 100
end
defd user_completion_rate(user) do
completed = length(user.completed_tasks)
total = length(user.all_tasks)
# Wrap the regular def function call with non_dx
non_dx(calculate_percentage(completed, total))
end
end
"""
def non_dx(code) do
code
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule Dx.MixProject do

defp package do
[
description: "Inference engine written in Elixir",
description: "Automatic data loading for Elixir functions",
files: ["lib", "mix.exs", "README*", "VERSION"],
maintainers: ["Arno Dirlam"],
licenses: ["MIT"],
Expand Down Expand Up @@ -69,7 +69,7 @@ defmodule Dx.MixProject do

defp docs do
[
main: "Dx",
main: "Dx.Defd",
extra_section: "Guides (old)",
extras: Path.wildcard("docs/**/*.md"),
groups_for_extras: [
Expand Down

0 comments on commit 2c58cf0

Please sign in to comment.