I’m trying to build a macro for edge definitions in a graph. How can I access parameters from the outer macro inside the inner macro?
- The direction macro must define a module with the block passed into schema.
- The schema macro must implicitly be able to access the “accesses” string passed into the top-level edge macro.
How can I define complex multi-level macros like this for building DSLs but have the state be shared like I described?
This is the edge module:
defmodule App.Edge.Accesses do
import App.Edge.EdgeType
alias App.Users.User
alias App.Invitations.Invitation
alias App.Tenants.Tenant
edge "accesses" do
direction from: User, to: Invitation
direction from: Invitation, to: Tenant
schema do
end
end
end
This is the EdgeType module with all of the macros:
defmodule Quota.Edge.EdgeType do
defmacro edge(collection_name, do: do_block) do
quote do
end
end
defmacro direction(from: from, to: to) do
quote do
end
end
defmacro schema(do: do_block) do
quote do
end
end
end
A somewhat common option often used for “contexts” like this is to use module attributes, where each macro would push their context into the attribute prior to injecting the body block and then pop it out after. Each inner macro can access the current context through that attribute.
I’m on my phone so can’t easily provide a more complete example, but basically the idea is to inject a call to Module.put_attribute
at the beginning of the quote block, and then
Here’s an example (untested):
@edge_context :"#{__MODULE__}.edge"
defmacro edge(collection, do: quoted) do
quote do
prev = Module.get_attribute(__MODULE__, unquote(@edge_context), [])
Module.put_attribute(__MODULE__, unquote(@edge_context), [unquote(collection) | prev])
result = unquote(quoted)
Module.put_attribute(__MODULE__, unquote(@edge_context), prev)
result
end
end
defmacro schema(do: quoted) do
context = Module.get_attribute(__CALLER__.module, @edge_context)
...
end
1 Like
In spark, this DSL would look something like this:
defmodule App.Edge.EdgeType.Dsl do
@schema ... (left out for brevity)
@direction %Spark.Dsl.Entity{
name: :direction,
schema: [
from: [type: :module, required: true],
to: [type: :module, required: true]
]
}
defmodule Edge do
defstruct [:name, :directions]
end
@edge %Spark.Dsl.Entity{
name: :edge,
target: Edge,
schema: [
name: [
type: :atom,
required: true
],
entities: [
directions: [@direction],
schema: [@schema]
],
singleton_entity_keys: [:schema]
]
}
@edges %Spark.Dsl.Section{
name: :edges,
top_level?: true,
entities: [
@edge
]
}
use Spark.Dsl.Extension, sections: [@edge]
end
defmodule App.Edge.EdgeType do
use Spark.Dsl,
default_extensions: [extensions: [App.Edge.EdgeType,Dsl]]
end
defmodule EdgeType.Info do
use Spark.InfoGenerator, extension: App.Edge.EdgeType.Dsl, sections: [:edge]
end
With the above (I left some stuff out, and probably got a few things wrong since I typed this all in a forum window and didn’t test it), the syntax you’ve laid out should work as it is.
defmodule App.Edge.Accesses do
use App.Edge.EdgeType # but you'd need to `use` instead of `import`
alias App.Users.User
alias App.Invitations.Invitation
alias App.Tenants.Tenant
edge "accesses" do
direction from: User, to: Invitation
direction from: Invitation, to: Tenant
schema do
end
end
end
3 Likes
Thank you, is there a way to defer evaluation of everything inside the schema block? When I add the following code, it give me issues as Ecto.Schema is not imported into the App.Edge.Accesses module.
defmodule App.Edge.Accesses do
use App.Edge.EdgeType
alias App.Users.User
alias App.Invitations.Invitation
alias App.Tenants.Tenant
edge "accesses" do
direction(from: User, to: Invitation)
direction(from: Invitation, to: Tenant)
schema do
field :role, Ecto.Enum, values: [:owner, :editor, :viewer]
edge_fields()
timestamps(type: :utc_datetime)
end
end
end
oh, interesting, I didn’t realize that was meant to be just a regular ecto schema. In Ash we define the ecto schema for Ash.Resource
out-of-band. What is edge_fields()
meant to do? I’m not sure what the best way is to accomplish what you want TBH.
So, to answer the specific question, you can add imports
to the edge entity, i.e imports [Ecto.Schema]
. But I feel like you might have a hard time making edge_fields()
work.
I’ll have to think about it a bit more 