Import Slice Component into `App`

This issue was previously raised by @aaricpittman here, but no solution was ever posted. I am now facing the same issue myself.

I would like inject components from one or more slices into actions that live in app. I can’t get it to work. I can reference the slice components with constants, but then I lose the benefit of containers and auto-inject.

The method for importing slice components from another slice using files placed in config/slices/ does not work for app.

  • If I create, for example, Bookshelf::Slice here, it will cause a TypeError (superclass mismatch for class App).
  • If I create, Bookshelf::App here, it will cause a NameError in the SliceRegistrar when it tries to work with a nonexistent Bookshelf::Slice.
  • For completeness, I will note that the usual methods work just fine for importing/exporting between slices in my project.

Placing the import method call in config/app.rb also does not work.

  • Placing the import method call in config/app.rb causes a NilClass error because Slice.import tries to access the current slice’s parent, which for app is non-existent.
  • I thought overriding the import method in Hanami::App to reference it’s own slices collection (instead of .parent’s collection) would work, but it causes a Hanami::SliceLoadError (Slice ‘main’ not found). I assume the other slices are apparently not available at the point where import is called.

Based on the above, it seems that importing slice components into app is not a supported behavior at this time. Is this intended? or is their another way to accomplish this kind of import?

If this is not intended behavior, I would be happy to work on a solution, but I will need some guidance.

Thank you!

Thanks for raising this, Damian! You’re right in presuming that this isn’t an arrangement we have designed for.

I’d be happy to have a look at what options might be possible here, but it won’t be until next week at the earliest.

In the meantime, I’d like to understand what you’re trying to do here. Can you share a (sanitised if necessary) overview of your app structure and why you want slice components to be imported into the app?

Of course, with the default behaviour of app components importing into slices, if you then going an import slice components into the app, then you have a cyclic dependency, which we all know is best avoided. This alone does give me pause about whether this arrangement is something we want to support.

So I wonder if another option here might be good for you: to shift most of your app code into its own separate slice. At that stage you can import the components from slice-to-slice just like we know is already possible. Is there any reason you’d prefer not to do that?

Thanks!

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 :roll_eyes: ).

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! :partying_face:) 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. :roll_eyes: 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. :pensive: But it allows me to maintain my organizational “purity.” :angel:

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! :tada:

Hi again Damian, I’ve had a chance to look at this now, and I don’t think we want to provide first-class support for the arrangement you’re suggesting, at least not now.

The Hanami app (i.e. the components loaded from the code in app/) serves the following purposes:

  • House the entirety of your app logic, either when starting small or when simply preferring a blended environment
  • When working with slices, to act as their parent and manage their loading
  • And to provide a common set of components/behaviour to those slices

Given this, app/ is a reasonable place to put your core business logic, to be consumed by one or more slices that then deliver that logic via various interfaces, but not the other way around. And as you’ve discovered, if you try and do it the other way around, you’re going against the grain of Hanami’s features and will generally have an awkward time.

I think you’re probably over-indexing on the primacy of the “upper case app”, i.e. the Hanami framework feature of the app/ folder and the Hanami.app object. For what you want to achieve, there’s nothing particularly special about this. Everything that works in the app can work just as well in a slice.

I’d also like to remind you that there is another much more important concept at play here, which is your “lower case Ruby app”, i.e. the totality of all the code within your Hanami codebase!

Your Ruby app can consist of whatever arrangement of slices you like! You don’t need an app/ to create a core/shell architecture. Go and put things in a “core” or “domain” slice instead, and it’s still part of your Ruby app. You haven’t “cheated” or done anything lesser from Hanami’s perspective. In fact, given Hanami’s current features, I’d say this is a better structure for future growth, since code can be more mobile between slices.

So to get back to your concrete example, I’d suggest you:

  1. Keep your “core” code in your “main” slice (feel free to rename it to “core” if you want to be really explicit about it)
  2. Move the relevant code from app/ into a “web” slice that then imports the components from the “main”/“core” slice (having a "core." prefix for those imported component keys feels better to me personally, but the choice is yours)
  3. Stop putting stuff in app/ unless it’s for one of the reasons I stated up top, i.e. sharing something simply with every slice

I hope this helps!

1 Like

Hi, @timriley. Thanks for taking the time to look into this.

I think I will follow your advice to create a “web” slice to hold the “shell” portions of my app. I look forward to exploring further and sharing my experiences.

Thanks again for your work on Hanami!

2 Likes