Rubanok Ruby DSL

repository·master·Indexed 42 days ago

https://github.com/palkan/rubanok

A Ruby DSL for building parameter-based data transformers. Rubanok allows developers to decouple collection manipulation logic—such as filtering, sorting, and pagination—from controllers by encapsulating it within dedicated Rubanok::Processor classes. It features a flexible DSL with map, match, and prepare methods, supports nested processors via .process, and provides integration helpers like rubanok_process and rubanok_scope for Rails controllers.

Tokens
6.9K
Snippets
27
Records
34
Agent score
51%

What's inside Rubanok

  1. What is a Rubanok Processor?

    master

    A Processor (formerly called plane or hand plane) is the core abstraction in Rubanok. It is a class responsible for mapping input parameters to specific data transformations. You define a processor by inheriting from Rubanok::Processor and using DSL methods like map, match, and prepare to describe how parameters should modify a data source (the raw input).

    class CourseSessionsProcessor < Rubanok::Processor
      map :q do |q:|
        raw.search(q)
      end
    end
  2. Configure rule activation behavior

    master

    By default, Rubanok requires all keys specified in a map or match rule to be present in the parameters to activate the rule. You can customize this:

    • activate_always: true: The rule is applied even if the keys are missing (useful for setting defaults).
    • activate_on: :key_name: The rule is activated based on the presence of a specific key, even if other keys in the rule are missing.
    • ignore_empty_values: true: If set, Rubanok will not run the rule if the parameter value is empty (e.g., nil or ""). This is enabled by default globally via Rubanok.ignore_empty_values = true.
  3. Integrate Rubanok into Rails controllers

    master

    Rubanok provides a rubanok_process helper for Rails controllers. It automatically attempts to find a processor class based on the controller name (e.g., CourseSessionsController looks for CourseSessionsProcessor).

    Get recognized parameters with rubanok_scope

    In Rails, you can use rubanok_scope to retrieve the hash of parameters that were actually recognized and used by the processor. This is helpful for displaying active filters in your views.

    class CourseSessionController < ApplicationController
      def index
        @sessions = rubanok_process(CourseSession.all)
        # Returns the Hash of params recognized by the processor
        @sessions_filter = rubanok_scope
      end
    end
    # In a controller
    @sessions = rubanok_process(CourseSession.all)
    @sessions_filter = rubanok_scope
  4. Test Rubanok processors with RSpec

    master

    You can test processors in isolation by calling .call(input, params). If you are using Rails, you can also use the have_rubanok_processed matcher in your controller specs.

    To use the matcher, add require "rubanok/rspec" to your spec_helper.rb or rails_helper.rb.

    RSpec.describe CourseSessionsProcessor do
      let(:input) { CourseSession.all }
      let(:params) { { q: "wood" } }
    
      subject { described_class.call(input, params) }
    
      specify "searching" do
        expect(subject).to eq input.search("wood")
      end
    end
  5. Configure Rubanok for RBS/Steep type checking

    master

    Rubanok includes Ruby type signatures (RBS). To use them with the Steep type checker, add library "rubanok" to your Steepfile.

    Because Rubanok uses a DSL with implicit context switching via instance_eval, you must provide explicit type hints (annotations) to help the type checker identify the current context within DSL blocks. For example, when inside a map block, you may need to annotate self as Rubanok::Processor, and inside a match block, annotate self as Rubanok::DSL::Matching::Rule.

    # In your Steepfile
    library "rubanok"
    
    # In your Processor
    class MyProcessor < Rubanok::Processor
      map :q do |q:|
        # @type self : Rubanok::Processor
        raw
      end
    
      match :sort_by, :sort, activate_on: :sort_by do
        # @type self : Rubanok::DSL::Matching::Rule
        having "status", "asc" do
          # @type self : Rubanok::Processor
          raw
        end
      end
    end
  6. How to build a data transformer with Rubanok::Processor

    master

    Rubanok provides a DSL to build parameters-based data transformers by subclassing Rubanok::Processor. You define transformations using the map method, which takes a key and a block. Inside the block, you receive a keyword argument representing the parameter value. You can also access the original input data via the raw accessor.

    To use a processor in a Rails controller, you can call the rubanok_process method, passing the data you want to transform.

    class CourseSessionProcessor < Rubanok::Processor
      map :q do |q:|
        raw.search(q)
      end
    end
    
    # In a controller
    class CourseSessionController < ApplicationController
      def index
        @sessions = rubanok_process(CourseSession.all)
      end
    end
  7. Infer Rubanok Processor classes from controllers

    master

    Rubanok uses a naming convention to automatically link controllers to their corresponding Processor classes. It takes the controller_path, classifies it, pluralizes it, and appends Processor.

    Example Mapping:

    • PostsController $\rightarrow$ PostsProcessor
    • Admin::UsersController $\rightarrow$ Admin::UsersProcessor (depending on path classification)

    If the inferred class cannot be found, you must provide the class explicitly using the with: option in rubanok_process or rubanok_scope.

  8. Test Rubanok transformations with RSpec matchers

    master

    Rubanok provides custom RSpec matchers to verify that a specific processor was called during a transformation block. This is useful for ensuring that your data processing pipeline correctly invokes the expected logic.

    Usage Pattern

    You use the have_rubanok_processed matcher (or its alias have_planished) within a block expectation. You must specify the processor you expect to be called using the .with modifier.

    1. have_rubanok_processed(data_class): Verifies that the processor was called with an instance of data_class (or the class itself).
    2. .with(processor): Specifies the processor instance/class that should receive the :call message.
    3. .as(alias_name): Allows customizing the matcher name in failure messages.

    Example

    # Assuming 'MyProcessor' is the processor you want to test
    expect { 
      # code that triggers Rubanok transformation 
    }.to have_rubanok_processed(MyDataClass).with(MyProcessor)
    
    # Or using the alias
    expect { 
      # code that triggers Rubanok transformation 
    }.to have_planished(MyDataClass).with(MyProcessor)
    expect { 
      # code that triggers Rubanok transformation 
    }.to have_rubanok_processed(MyDataClass).with(MyProcessor)
  9. Implement a data transformer using Rubanok::Processor

    master

    To transform data, inherit from Rubanok::Processor and define transformation rules using the map and match DSL methods.

    Rules are applied by calling the class method .call(input, params). The input is the data to be transformed, and params is a Hash containing the criteria for the transformation.

    Inside a map block, you can access the original input data using the raw method. The map block defines a method that is executed when its corresponding match criteria are met.

    class MyTransformer < Rubanok::Processor
      # Define which params trigger this rule
      match :type
    
      # Define the transformation logic
      map :type do
        # 'raw' provides access to the input data
        raw.where(type: type)
      end
    end
    
    # Usage:
    # input: MyModel.all
    # params: { type: "public" }
    result = MyTransformer.call(MyModel.all, { type: "public" })
  10. Use `prepare` for default transformations

    master

    The prepare method allows you to define a transformation that runs before any other rules are activated. This is useful for initializing a complex data structure (like an Elasticsearch query hash) that subsequent map or match rules will then modify.

    The block should return a new initial value for the raw input or nil if no transformation is required.

    Note: prepare is only executed if at least one parameter matches a rule.

    class CourseSearchQueryProcessor < Rubanok::Processor
      prepare do
        # Initialize a base structure
        {query: {bool: {filters: []}}}
      end
    
      map :ids do |ids:|
        raw.dig(:query, :bool, :filters) << {terms: {id: ids}}
        raw
      end
    end
  11. Define nested processors with `process`

    master

    You can define sub-processors using the process method. This is useful for handling nested parameter structures (e.g., params[:filter][:status]).

    class CourseSessionsProcessor < Rubanok::Processor
      process :filter do
        match :status do
          having "draft" do
            raw.where(draft: true)
          end
        end
      end
    end