Class Configs with Lambdas in Ruby

I’ve been getting reacquainted with Ruby, diving into a well established project which has been blessed by numerous smart developers over the course of the past 10 years. I discovered an interesting pattern for gathering models (ApplicationRecord classes) that may or may not be eligible for some feature: You start with a mixin that creates a method for your classes to pass options to; as well as a method for determining if those options enable the feature or not:

module ProvidesFeature 
    class_methods do 
        # pass this to the model class
        def features_provided(model, **opts)
            (@features ||= []) << [model, opts]
        end

        # call this to initialize class feature checks
        def feature_models(ctxt)
            features_provided.map do |args|
                DynamicFeature.new(ctxt, args)
            end
        end
    end 
end 

Here is an example DynamicFeature class instantiated above. This could be a bit simpler if you didn’t want to pass any context in but a lot of the power of this approach comes from the flexibility an argument like context gives you:

class DyanmicFeature do 
    def initialize(ctxt, config_args)
        @ctxt = ctxt
        configure(config_args)  
    end

    def configure(ctxt, args = {})
        @should_provide_feature = args.fetch(:should_feature_be_provided) do 
            -> (ctxt) { ctxt&.fetch(:person_is_admin, false) }
        end
    end 

    def can_feature?
        @should_provide_feature.call(@ctxt)
    end
end 

Pausing for a moment and breaking this down. The #configure method is the main source of the magic. First we try to get the keyword :should_feature_be_provided (implemented below). If we get it we can return it’s value; however, there is built in flexibility to this. If args does not have a :should_feature_be_provided key then we can call a lambda with additional context. Again, you don’t need to pass anything else but I view this flexability as a strength if used strategically. Now implement; in an active record ie. Person

class Person < ApplicationRecord 
    include ProvidesFeature 

    features_provided :person, 
        should_feature_be_provided: -> (ctxt) { ctxt.person.is_admin? }
    

You can then easily gather any models that ProvidesFeature:

ApplicationRecord.subclasses.select { |klass| klass < ProvidesFeature }

Instantiate DynamicFeature on each class (note we are passing some context that assumes there is a person with an is_admin? method. It’s a little contrived but it illustrates the point: you can pass additional context in when the feature_models are built.

.flat_map { |klass| klass.feature_models(ctxt) }

Then filter with can_feature?

.select { |klass| klass.can_feature? }

At the start of this post I said this was an “interesting pattern”; not necessarily saying it’s a good one. I’m still fairly new to Ruby (despite having built a few production projects back in 2016 and 2018) and the OO pattern. Personally; I found the above extremely difficult to grok and even though I understand it I’ve found that, within the context of the project I’m working on, I’ve myself treadmilling through various files. In some ways I feel like, clever, as it is, this pattern may obfuscate a little too much but I’m open to feedback from those who have been in the OO world longer.