Hi, @timriley. Thanks for your reply. I am keenly aware of how much things fall on you right now. I would like to help more, but I would need some direction. If that offer is appealing, I would love to talk further. I will continue to study the code and see if I have any ideas for resolving this particular issue.
tl;dr
I want to refactor from this (modified for publication) action that either renders an empty search page, or uses HTMX to live search (as the query is typed by the user).
# addressbook/app/actions/contacts/index.rb
module AddressBook
module Actions
module Contacts
class Index < AddressBook::Action
include Deps[
contact_repo: "repositories.contact",
contact_search_view: "views.contacts.contact_search"
]
params { optional(:query).filled(AddressBook::Types::StrippedString, min_size?: 2) }
def handle(request, response)
if request.get_header("HTTP_HX_TRIGGER") == "contact-search"
if request.params.valid?
query_terms = request.params[:query].split
search_result = contact_repo.search_by_name(query_terms)
response.render(contact_search_view, contacts: search_result)
else
response.status = :no_content
end
else
response.render(view)
end
end
end
end
end
end
To this action:
# frozen_string_literal: true
module AddressBook
module Actions
module contacts
class Index < AddressBook::Action
include Deps[
interactor: "main.contact_search_by_name", # this doesn't work :(
contact_search_view: "views.contacts.contact_search"
]
def handle(request, response)
if request.get_header("HTTP_HX_TRIGGER") == "contact-search"
params = request.params
result = interactor.(params)
case result
in Success(*contact_list)
response.render(contact_search_view, contacts: contact_list)
in Failure[:invalid_data, errors, error_msgs]
response.status = :no_content
else
response.status = :internal_server_error # is there a way to get rid of this catch-all branch?
end
else
response.render(view)
end
end
end
end
end
end
Since the component registration isn’t working, I have to define the interactor explicitly in #handle
:
interactor = Main::ContactFindByName
For completeness, my interactor looks like this:
# slices/main/contact_search_by_name.rb
module Main
class ContactSearchByName < AddressBook::Interactor
include Deps[ contact_repo: "adapters.repositories.contact" ]
params { required(:query).filled(AddressBook::Types::StrippedString, min_size?: 2) }
def handle(_user, params)
validated_params = yield validate(params)
search_terms = validated_params[:query].split
contact_list = yield search_by_name(search_terms)
Success(contact_list)
end
private
def validate(params)
# defined in parent class
end
def search_by_name(search_terms)
...
end
end
end
Details/explanation below.
I have tried to explain my overall approach a few times in other topics, but I don’t think I’ve done a very good job. My most recent try was here (where I fudged my description of containers/autoloading and you corrected me ).
Basically, I want to follow what appears to have been a supported pattern in Hanami 1. That is, to have “app” logic (Hanami actions) call “business” logic (Hanami interactors or something else) located elsewhere (probably under lib/
back then, but hopefully in slices right now).
My purpose is to follow the pattern of separating “app” logic from “business” logic (in the spirit of Gary Bernhardt’s Core/Shell or Hexagonal/Clean/Onion architecture). As such, I would like to keep my “app” in app/
, and my business logic in one or more slices. The app will handle all routes, actions, and views, and the slices will handle the business logic (and dependencies, like persistence).
I have created a simple but functional business object that gives me the same Params functionality of Hanami::Action and provides Do Notation and Result objects from Dry-rb. I called it ProtoInteractor (for no particular reason other than the previous Hanami Interactor). I can provide sample code for its usage, but that is probably far afield from the current issue.
My file structure is as you would expect, with vanilla actions and views, and so far just a “Main” slice. Within a slice, I would like to have my ProtoInteractor objects all in the root, along with an entities
directory (which I haven’t explored very far yet), and an adapters
directory to hold repositories and other dependencies. I also use Types, but these still live in lib/
for now. This is all working in my app at present.
Originally, I started with everything organized in app/
, à la DecafSucks. I ran into this issue when refactoring business logic from my existing actions to ProtoInteractors in the Main slice.
Cyclic dependencies can be a problem, but I don’t think the current arrangement prevents them. That is, I believe I can create cyclic dependencies between slices now (please correct me if I’m wrong).
I haven’t played at all with Hanami’s ability to nest slices, but it might also be possible to create cyclic dependencies up and down the slice hierarchy. Am I wrong about this? Are slices only allowed to call sibling and descendant slices? I will dig into the code more to try to understand.
For me, I intend to avoid cyclic dependencies by only calling slice ProtoInteractors from app
, and never importing app
components into a slice (other than the default “settings”, “notifications”, etc., and “persistence.rom” at the moment). I would also like to avoid importing between slices, but we’ll see how that goes.
In any event, I think that it would be better provide the ability to raise and alert for cyclic dependencies, rather than try to prevent them outright. I look forward to having static analysis tools in Hanami, similar to Packwerk, that will detect obvious cyclic dependencies and warn of those and other “boundary” violations. (It’s gonna be awesome! ) This approach will provide more flexibility to developers, while providing the tools for them to diagnose and address problems when they occur.
I appreciate this advice. @swilgosz made the same point in another post. I would prefer not to do this if I can avoid it. This might just be my idealism run amok. Another option–what I am using right now–is to forego component registration and auto-injection and reference the ProtoInteractors directly as symbols. Also not ideal. But it allows me to maintain my organizational “purity.”
Wrap Up
The current situation feels counter to the position that app
is “just another slice.” At present, we either have to rely on routing to get from the “app” layer to “slice” logic, or re-create an app
slice in slices/
and orchestrate from there. This seems to put developers in the position of having to choose between organizing their logic in app
(for simple/start-up apps like DecafSucks) or using slices. With the proposed change, developers can evolve smoothly and naturally from app-centric to slice-based projects.
I think that calling slice components from app
could be a core feature of Hanami. This one change will make Hanami powerfully flexible by giving developers myriad options for organizing their code. Over time, I believe the “Core & Shell” pattern will prove more popular than splitting actions and views into “vertical slices” (see, e.g., https://github.com/bkuhlmann/hemo/).
Core & Shell allows actions and views to split along different seams than business logic. Having them all together in slices means there will end up being slices for UI (divided up for UI reasons, like admin vs. user) and other slices for business stuff (divided according to domain reasons). Two reasons for dividing means four times as many slices (or worse). This hyper-fragmentation was a common experience with CBRA Rails apps (I can give more references for this). Field examples of Packwerk usage from Gusto and others appear to be following more of a “Core & Shell” pattern, with “Shell” concepts either in the parent Rails app or separate “Packs” (the latter still not ideal, in my view).
By separating Core from Shell, slices can focus entirely on the business domain and can be organized solely around those domain concerns. I don’t feel that my practice of keeping all actions/views in app/
will be restrictive, since actions/views can adequately be divided and organized with namespaces under app/
. Finally, I think that the Core & Shell approach is also consistent with the position that a Hanami app can be anything, not just a web app: keeping whatever “endpoints” the app needs in app/
allows slices to focus exclusively on the core functionality of the app.
Thanks again for your help! All of the above is solely for the purpose of discussion. I hope you don’t take any of this as criticism of the wonderful work you’ve done with Hanami 2! Nothing but respect! I look forward to using Hanami for many years to come!
BONUS: As a coding exercise for myself, I refactored Brooke Kuhlmann’s Hemo app from slice-based to app-centric (like DecafSucks). I could refactor again to a Core & Shell pattern if comparing the three would be informative. It’s not a big app, so the example would be limited, but let me know if you think this would be useful.
Cheers!