Developing the library I announced recently, I took inspiration from dry-transaction
in order to design and implement its DSL layer. I started using the very same implementation in dry-transaction
and refactored it further until I got a clear model and understanding of what I was doing.
It can be summarized with the following pattern generalized for a library of name A
:
-
include A
just delegates to includingA.()
, which in turns delegates to including anA::Builder
instance. -
Consequently, a way to pass extra arguments is to directly
include A.(args)
, which will pipe the arguments to theA::Builder#initialize
method. -
The instance of
A::Builder
will do two things to the host class:- Extend with an instance of a
A::ClassContext
. - Include an
InstanceMethods
module.
- Extend with an instance of a
-
A::ClassContext
instance will define the state of the host class, which comes from two different sources:-
Original options passed to
A.()
. -
Any DSL it can define, which at the same time can be extracted to a
A::DSLContext
instance.
Both types of states will be defined as singleton methods in the
A::ClassContext
instance, this way the host class will have access to them as it is being extended by it. -
-
A::InstanceMethods
will define an initializer which, along with any other needed processing, will take all the state defined at the class level (which as I said comes fromA::ClassContext
) and copy it as its own instance state. -
A
will become usable by initializing it with any required argument and then calling any method defined inA::InstanceMethods
.
I won’t blame you I you haven’t read until here Anyway, you can see an example in my library:
What I want to re-mark here is that maybe here we have a common pattern which can be reused. The only variables which would change from library A to library B are:
- State defined as arguments of
Module.()
. - State defined in
Module::DSLContext
. - Extra arguments and state processing of
Module::InstanceMethods#initialize
. - Extra methods defined in
Module::InstanceMethods
So, this message is to gather other views on this. Do you think that this model would encompass all the needs that arise when developing a DSL or, at least, be a good starting point which could be easily extended? Or am I being short-sighted here focusing only in the use case I faced? Do you think it could implement DSL’s used across dry-rb
libraries? It would be a good thing in order to be consistent across libraries (right now they differ: include Dry.Types()
, include Dry::Transaction
, extend Dry::Initializer
… maybe even < Dry::Struct
?). It is not something I could work on right now, but it could be an idea for the near future…