0

The Backstory:
I'm writing a Rails engine that provides an API for advanced search and reporting capabilities against the app's models. Currently the engine has a hard-coded list of model classes. It then pulls every single column out of those models (using model.columns etc.) and makes them searchable and displayable through the searching/reporting UI. The search query (a Ransack hash) is created by the user through the UI.

I need a way to define which models/columns are searchable and which aren't (eg. letting users search and create reports using the password digest column would be a bad idea). The engine needs to be able to to read the configuration data and restrict certain models/columns from even showing up in the UI.

The Question:
What's the right way to have classes in Rails provide configuration information to a separate class/module/service?

The Complication:
1) It seemed at first like I could just include a module into the models and use a method at the class level to do the configuration, like so:

class TheModel < ActiveRecord::Base
  include TheMixin

  searchable_columns :id, :first_name, :last_name

The problem with this approach, however, is that in the development environment Rails lazy-loads classes and thus the configuration information wouldn't get registered with TheMixin unless TheModel is required for some reason. Although it would still work in the production environment developing the Engine or any app that uses it would be a headache. This makes me wonder if defining the information through methods run at class definition is a code smell.

So due to Rails' lazy-loading the configuration information needs to be present before the search service is first run. Hardcoding the information into the service is not an option since it's an Engine, so what other options are left?

2) One option would be putting the information into an initializer, but that seems like a code smell as well. I don't want to have the configuration so far decoupled from the classes themselves because that seems like a maintenance issue. It also seems like the configuration information should be a concern of the model and therefore handled by the model class rather than some initializer.

3) The last option I see is making the information available on the model itself such as in the form of a method:

def search_config
  { columns: [:id, :first_name, :last_name] }
end

and then in the service, getting a list of all the models and looping through them to get the configurations. But then this also feels like a code smell. It requires writing code specifically for the development environment (Rails.application.eager_load!), forces eager loading, and feels like it could easily be broken by edge cases. Furthermore, at this point we've already forced all of the models to load, so why not just use the first mixin-style option and just use Rails.application.eager_load! on its own? That would trigger the class-level methods and get us our configuration.

EDIT: It's also occurred to me that defining configuration at initialization time is a bad idea. What if the user should be able to search the Users table if they're an admin but not if they're a standard user? This wouldn't be possible if configuration was defined at initialization time. It seems like the best option would be if the models could register themselves with the search service at initialization time but then the search service would query each registered module for configuration information each time it runs, allowing the models to define conditional configurations. Of course, there's still the problem of eager loading and the development environment...

Community
  • 1
  • 1
mayhewluke
  • 133
  • 1
  • 6
  • have a look at this gem for ideas about how to have a service that requires information about models searchable attributes (https://github.com/sunspot/sunspot) – nort Jul 18 '14 at 23:31
  • Thanks, I see there's a lot of good info there, and it seems to confirm the validity of the mixin idea. I noticed that they have a [Rake task](https://github.com/sunspot/sunspot/blob/master/sunspot_rails/lib/sunspot/rails/tasks.rb#L33) that uses `eager_load!`, but it doesn't answer how to handle running the app/engine in the `development` or `test` environments, unfortunately. – mayhewluke Jul 19 '14 at 00:09

0 Answers0