docile
docile copied to clipboard
Use docile with root class declaration
Hi. Thanks for all your work!
I am the maintainer of https://github.com/avo-hq/avo. Avo is an admin panel framework where you can build apps very fast! It achieves that through an extendable and flexible DSL.
This is just a sample. The result of this you can visit here.
class PostResource < Avo::BaseResource
field :id, as: :id
field :name, as: :text, required: true, sortable: true
field :body, as: :trix, placeholder: "Enter text", always_show: false, attachment_key: :attachments, hide_attachment_url: true, hide_attachment_filename: true, hide_attachment_filesize: true
field :cover_photo, as: :file, is_image: true, as_avatar: :rounded, full_width: true, hide_on: []
field :cover_photo, as: :external_image, name: "Cover photo", required: true, hide_on: :all, link_to_resource: true, as_avatar: :rounded, format_using: ->(value) { value.present? ? value&.url : nil }
field :audio, as: :file, is_audio: true
field :excerpt, as: :text, hide_on: :all, as_description: true do |model|
ActionView::Base.full_sanitizer.sanitize(model.body).truncate 130
rescue
""
end
field :is_featured, as: :boolean, visible: ->(resource:) { context[:user].is_admin? }
field :is_published, as: :boolean do |model|
model.published_at.present?
end
field :user, as: :belongs_to, placeholder: "—"
field :status, as: :select, enum: ::Post.statuses, display_value: false
field :comments, as: :has_many
filter PostStatusFilter
action TogglePublished
tool PostInfo
end
I'd like to change the way we build the DSL. At the moment we use static methods to get the field
s and build the final object.
Now, we're introducing wrappers like tab
or panel
.
class PostResource < Avo::BaseResource
panel "Main info" do
field :name, as: :text, required: true, sortable: true
field :body, as: :trix, placeholder: "Enter text", always_show: false, attachment_key: :attachments, hide_attachment_url: true, hide_attachment_filename: true, hide_attachment_filesize: true
end
tab "Files" do
field :cover_photo, as: :file, is_image: true, as_avatar: :rounded, full_width: true, hide_on: []
field :audio, as: :file, is_audio: true
end
end
I know that you usually have to give docile a block with the DSL, but is there a way to use docile and pick up the fields from the resource root class like above?
Thank you!
Cool @adrianthedev! I appreciate your interest in Docile.
Can you please say a bit more about what you are hoping to accomplish? Are you saying that there is a problem implementing the desired DSL with Docile today?
Thanks for the quick answer.
What I'm trying to achieve is to be able to evaluate everything inside the class as a block for docile. I'll try to create an example today.
How I could use docile now:
class PostResource < Avo::BaseResource
self.fields = -> do
field :id, as: :id
field :name, as: :text
tab do
field :name, as: :text
end
end
end
dsl = Docile.dsl_eval(Builder.new, &PostResource.fields)
How I'd like to use it:
class PostResource < Avo::BaseResource
field :id, as: :id
field :name, as: :text
tab do
field :name, as: :text
end
end
dsl = Docile.dsl_eval(SOMETHING_HERE_TO_PICK_UP_THE_FIELDS)
Ok, I am looking at these two files to understand what exists today:
What would happen if you tried to introduce the tab
method, still in the pattern of Class methods you have, like this:
# File: lib/avo/base_resource.rb
# Class: Avo::BaseResource
class << self
def tab(&block)
# do other stuff...
Docile.dsl_eval(self, &block)
end
end
Now, I'm not sure if you are intending to model in your data structure that the DSL methods (like field
) were called from inside the definition of a tab
?
If so, to make the DSL map to that data structure, you are probably going to have to make all of the class method DSL calls refer to some shared state such as a stack of contexts, if that makes sense?
So perhaps, you might have something like:
def tab(**args, &block)
# assume this starts as [], and we stack up contexts
dsl_context_stack.push args.merge(wrapper_type: tab)
# Run against the class itself, but EVERY class method will refer to context
Docile.dsl_eval(self, &block)
# now we pop from the stack
dsl_context_stack.pop
end
def dsl_context
dsl_context_stack.last || {}
end
def field(field_name, as:, **args, &block)
# merge from dsl_context as well as any args[:context]
merged_context = dsl_context.merge(args.fetch(:context, {}))
# Do normal stuff for fields, but also checking...
if context[:wrapper_type] == :tab
# ...
end
end
This would seem to be achievable from where your code is today, perhaps without overwhelming changes? Please let me know if this is helpful, or if I have misunderstood the issues at hand?
Again, thanks for looking into this. I appreciate it!
The most contrived example of the API I'm looking for is this one:
class UserResource < Avo::BaseResource
field :first_name
panel do
field :last_name
end
# One set of tabs together
tabs do
tab :Main do
field :email
field :email
tool UserTool
end
tab :Other do
tool MuserTool
end
end
# Panel in between the tabs
panel do
field :first_name
tool :last_name
end
# Another set of tabs together
tabs do
tab :Another_One do
field :email
field :email
tool UserTool
end
tab :Other_One do
tool MuserTool
end
end
end
What's to note here is that the tabs
, tab
, and panel
are repeated through the same resource.
What I'm doing to hold their contents is instantiate a PORO and stack up the items (field
, tool
).
So, when I use field
in a tab
or a panel
, I use an instance method on the object (the self
in Docile.dsl_eval(self, &block)
).
But when ruby reads the resource file and hits the field
method, that needs to be a class method. That leads to code duplication.
I was hoping I could get everything in between class UserResource < Avo::BaseResource
and the last end
as a block that I could push through to docile.
I think that's not possible (to take everything in between and just use it as a block) and I'm going a different route.
I'll post my solution here when done.
Thank you!
I'm wondering, What if...
- Move the class-level DSL methods to be instance methods of a DSL class
- Each class automatically gets a class-level instance of the DSL class
- BaseResource class methods are delegated to the class-level instance of the DSL class
This might allow the easier creation of a nested multi-level DSL, because the top-level class methods would be delegated to an instance, which can then create new instances for each tab/panel block?
Also, do you feel comfortable separating a Builder class from the final data structure that is built? This may make your code ultimately cleaner and easier to understand.
Best of luck! Hope this helps
Hey man, I returned to the task today and tried your suggestions above and they worked out amazing!
It didn't really come out as above, but very close. I was able to re-use the field
methods for static and instance scenarios. I'm delegating to an instance DSL class.
I'll publish clean code soon but there's something very messy here (extremely messy). Thanks for the suggestions!