Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use docile with root class declaration #98

Open
adrianthedev opened this issue May 23, 2022 · 10 comments
Open

Use docile with root class declaration #98

adrianthedev opened this issue May 23, 2022 · 10 comments

Comments

@adrianthedev
Copy link

adrianthedev commented May 23, 2022

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 fields 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!

@ms-ati
Copy link
Owner

ms-ati commented May 23, 2022

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?

@adrianthedev
Copy link
Author

adrianthedev commented May 23, 2022

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)

@ms-ati
Copy link
Owner

ms-ati commented May 23, 2022

Ok, I am looking at these two files to understand what exists today:

@ms-ati
Copy link
Owner

ms-ati commented May 23, 2022

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

@ms-ati
Copy link
Owner

ms-ati commented May 23, 2022

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

@ms-ati
Copy link
Owner

ms-ati commented May 23, 2022

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?

@adrianthedev
Copy link
Author

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.

@adrianthedev
Copy link
Author

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!

@ms-ati
Copy link
Owner

ms-ati commented May 24, 2022

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

@adrianthedev
Copy link
Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants