From 1eaf511818e264bad54364973c796490f50b169c Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Thu, 11 Jan 2024 21:46:10 +0100 Subject: [PATCH] feat: allow finders to be inherited feat: change configuration from hash to configuration object --- .../finder/activities/prepare_adapter.rb | 2 +- .../finder/activities/prepare_entity.rb | 8 +- .../finder/activities/prepare_filters.rb | 8 +- .../finder/activities/prepare_paging.rb | 10 +- .../finder/activities/prepare_properties.rb | 12 +- .../finder/activities/prepare_sorting.rb | 12 +- .../finder/activities/process_adapters.rb | 7 +- lib/trailblazer/finder/dsl.rb | 98 ++++++---- test/support/operations.rb | 12 ++ test/trailblazer/finder/dsl_spec.rb | 173 ++++++++++++++++++ test/trailblazer/finder/dsl_test.rb | 12 +- test/trailblazer/operation/finder_spec.rb | 16 +- 12 files changed, 301 insertions(+), 69 deletions(-) create mode 100644 test/trailblazer/finder/dsl_spec.rb diff --git a/lib/trailblazer/finder/activities/prepare_adapter.rb b/lib/trailblazer/finder/activities/prepare_adapter.rb index 0071758..e93d5ac 100644 --- a/lib/trailblazer/finder/activities/prepare_adapter.rb +++ b/lib/trailblazer/finder/activities/prepare_adapter.rb @@ -5,7 +5,7 @@ class Finder module Activities class PrepareAdapter < Trailblazer::Activity::Railway def set_adapter(ctx, **) - ctx[:adapter] = ctx.dig(:config, :adapter) || "Basic" + ctx[:adapter] = ctx[:config].adapter end def validate_adapter(_ctx, adapter:, **) diff --git a/lib/trailblazer/finder/activities/prepare_entity.rb b/lib/trailblazer/finder/activities/prepare_entity.rb index 122f9b2..a39a170 100644 --- a/lib/trailblazer/finder/activities/prepare_entity.rb +++ b/lib/trailblazer/finder/activities/prepare_entity.rb @@ -4,16 +4,16 @@ module Trailblazer class Finder module Activities class PrepareEntity < Trailblazer::Activity::Railway - def validate_entity(ctx, **) - ctx.dig(:options, :entity) || ctx.dig(:config, :entity) + def validate_entity(ctx, config:, **) + ctx.dig(:options, :entity) || config.entity end def invalid_entity_error(ctx, **) (ctx[:errors] ||= []) << {entity: "Invalid entity specified"} end - def set_entity(ctx, **) - ctx[:entity] = ctx.dig(:options, :entity) || instance_eval(&ctx[:config][:entity]) + def set_entity(ctx, config:, **) + ctx[:entity] = ctx.dig(:options, :entity) || instance_eval(&config.entity) end step :validate_entity diff --git a/lib/trailblazer/finder/activities/prepare_filters.rb b/lib/trailblazer/finder/activities/prepare_filters.rb index 11709b4..74308cf 100644 --- a/lib/trailblazer/finder/activities/prepare_filters.rb +++ b/lib/trailblazer/finder/activities/prepare_filters.rb @@ -4,8 +4,8 @@ module Trailblazer class Finder module Activities class PrepareFilters < Trailblazer::Activity::Railway - def validate_filters(ctx, **) - filters = ctx.dig(:config, :filters) + def validate_filters(_ctx, config:, **) + filters = config.filters filters.each do |key, _value| return false if !filters[key][:with].nil? && !filters[key][:with].is_a?(Symbol) end @@ -16,8 +16,8 @@ def invalid_filters_error(ctx, **) (ctx[:errors] ||= []) << {filters: "One or more filters are missing a with method definition"} end - def set_filters(ctx, **) - ctx[:filters] = ctx[:config][:filters] + def set_filters(ctx, config:, **) + ctx[:filters] = config.filters end step :validate_filters diff --git a/lib/trailblazer/finder/activities/prepare_paging.rb b/lib/trailblazer/finder/activities/prepare_paging.rb index 44ffd10..8ad9f83 100644 --- a/lib/trailblazer/finder/activities/prepare_paging.rb +++ b/lib/trailblazer/finder/activities/prepare_paging.rb @@ -4,15 +4,15 @@ module Trailblazer class Finder module Activities class PreparePaging < Trailblazer::Activity::Railway - def check_paging(ctx, **) - paging = ctx[:config][:paging] || nil - return false if ctx[:config][:paging].empty? || paging.nil? + def check_paging(_ctx, config:, **) + paging = config.paging + return false if config.paging.empty? || paging.nil? true end - def set_paging(ctx, **) - ctx[:paging] = ctx.dig(:config, :paging) || {} + def set_paging(ctx, config:, **) + ctx[:paging] = config.paging ctx[:paging][:current_page] = ctx.dig(:params, :page) || 1 return true unless ctx[:params][:per_page] diff --git a/lib/trailblazer/finder/activities/prepare_properties.rb b/lib/trailblazer/finder/activities/prepare_properties.rb index 2c84bdd..8d5f5be 100644 --- a/lib/trailblazer/finder/activities/prepare_properties.rb +++ b/lib/trailblazer/finder/activities/prepare_properties.rb @@ -4,8 +4,8 @@ module Trailblazer class Finder module Activities class PrepareProperties < Trailblazer::Activity::Railway - def check_property_types(ctx, **) - properties = ctx[:config][:properties] || {} + def check_property_types(_ctx, config:, **) + properties = config.properties return true if properties.empty? properties.each do |key, _value| @@ -13,8 +13,8 @@ def check_property_types(ctx, **) end end - def validate_property_types(ctx, **) - properties = ctx[:config][:properties] || {} + def validate_property_types(_ctx, config:, **) + properties = config.properties return true if properties.empty? properties.each do |key, _value| @@ -26,8 +26,8 @@ def invalid_properties_error(ctx, **) (ctx[:errors] ||= []) << {properties: "One or more properties are missing a valid type"} end - def set_properties(ctx, **) - ctx[:properties] = ctx[:config][:properties] + def set_properties(ctx, config:, **) + ctx[:properties] = config.properties end step :check_property_types diff --git a/lib/trailblazer/finder/activities/prepare_sorting.rb b/lib/trailblazer/finder/activities/prepare_sorting.rb index 8f29616..1a9541b 100644 --- a/lib/trailblazer/finder/activities/prepare_sorting.rb +++ b/lib/trailblazer/finder/activities/prepare_sorting.rb @@ -4,20 +4,20 @@ module Trailblazer class Finder module Activities class PrepareSorting < Trailblazer::Activity::Railway - def check_sorting(ctx, **) - sorting = ctx[:config][:sorting] || nil - return true unless ctx[:config][:sorting].empty? || sorting.nil? + def check_sorting(_ctx, config:, **) + sorting = config.sorting + return true unless sorting.empty? || sorting.nil? end - def set_sorting(ctx, **) + def set_sorting(ctx, config:, **) return true if ctx[:params][:sort].nil? sorting = ctx[:params][:sort] - config = ctx[:config][:sorting] + sorting_config = config.sorting ctx[:sorting] = ctx[:sorting] || {} sorting.split(",").each do |sorter| spt = sorter.split - ctx[:sorting][spt[0]] = fetch_sort_direction(config[spt[0].to_sym], spt[1]) if config.include?(spt[0].to_sym) + ctx[:sorting][spt[0]] = fetch_sort_direction(sorting_config[spt[0].to_sym], spt[1]) if sorting_config.include?(spt[0].to_sym) end end diff --git a/lib/trailblazer/finder/activities/process_adapters.rb b/lib/trailblazer/finder/activities/process_adapters.rb index 07d6111..7e790ad 100644 --- a/lib/trailblazer/finder/activities/process_adapters.rb +++ b/lib/trailblazer/finder/activities/process_adapters.rb @@ -15,7 +15,7 @@ def set_adapter((ctx, _flow_options), **) end def set_paginator(ctx, **) - paginator = ctx.dig(:config, :paginator) + paginator = ctx[:config].paginator return true unless paginator return false unless EXT_ORM_ADAPTERS.(ctx[:orm][:adapter]) return false unless PAGING_ADAPTERS.(paginator) @@ -26,10 +26,7 @@ def set_paginator(ctx, **) def invalid_paginator_error(ctx, **) (ctx[:errors] ||= []) << { - paginator: "Can't use paginator #{ctx.dig( - :config, - :paginator - )} without using an ORM like ActiveRecord or Sequel" + paginator: "Can't use paginator #{ctx[:config].paginator} without using an ORM like ActiveRecord or Sequel" } end diff --git a/lib/trailblazer/finder/dsl.rb b/lib/trailblazer/finder/dsl.rb index 19cf71c..a6bdd84 100644 --- a/lib/trailblazer/finder/dsl.rb +++ b/lib/trailblazer/finder/dsl.rb @@ -1,59 +1,95 @@ -# frozen_string_literal: true - module Trailblazer class Finder + class Configuration + attr_accessor :entity, :paging, :properties, :sorting, + :filters, :adapter, :paginator + + def initialize + @paging = {} + @properties = {} + @sorting = {} + @filters = {} + @paginator = nil + @adapter = "Basic" + end + + def clone + new_config = Configuration.new + new_config.entity = entity + new_config.paging = paging.clone + new_config.properties = properties.clone + new_config.sorting = sorting.clone + new_config.filters = filters.clone + new_config.adapter = adapter + new_config.paginator = paginator + new_config + end + end + module Dsl - attr_reader :config + def config + @config ||= Configuration.new + end + def inherited(base) - base.instance_variable_set "@config", apply_config({}) + ## We don't want to inherit the config from Trailblazer::Finder + return if name == 'Trailblazer::Finder' + + base.config = config.clone end def entity(&block) - config[:entity] = block + config.entity = block end - def paging(**options) - config[:paging][:per_page] = options[:per_page] || 25 - config[:paging][:min_per_page] = options[:min_per_page] || 10 - config[:paging][:max_per_page] = options[:max_per_page] || 100 + def paging(per_page: 25, min_per_page: 10, max_per_page: 100) + config.paging[:per_page] = per_page + config.paging[:min_per_page] = min_per_page + config.paging[:max_per_page] = max_per_page end def property(name, options = {}) - config[:properties][name] = options - config[:properties][name][:type] = options[:type] || Types::String - config[:sorting][name] = options[:sort_direction] || :desc if options[:sortable] + config.properties[name] = options + config.properties[name][:type] = options[:type] || Types::String + config.sorting[name] = options[:sort_direction] || :desc if options[:sortable] end def filter_by(name, options = {}, &block) filter_name = name.to_sym - config[:filters][filter_name] = {} - config[:filters][filter_name][:name] = name - config[:filters][filter_name][:with] = options[:with] if options.include?(:with) - config[:filters][filter_name][:block] = block || nil + config.filters[filter_name] = {} + config.filters[filter_name][:name] = name + config.filters[filter_name][:with] = options[:with] if options.include?(:with) + config.filters[filter_name][:block] = block || nil + end + + def adapter(adapter_name) + config.adapter = adapter_name.to_s + end + + def paginator(paginator_name) + config.paginator = paginator_name.to_s end - def adapter(adapter) - config[:adapter] = adapter.to_s + def current_adapter + config.adapter end - def paginator(paginator) - config[:paginator] = paginator.to_s + def current_paginator + config.paginator end - def apply_config(options, **) - return @config = options unless options.empty? + def filters_count + config.filters.count + end - @config = { - actions: {}, - entity: nil, - properties: {}, - filters: {}, - paging: {}, - sorting: {}, - adapters: [] - } + def properties_count + config.properties.count end + + protected + + attr_writer :config end end end diff --git a/test/support/operations.rb b/test/support/operations.rb index 0cc6652..ac26bf7 100644 --- a/test/support/operations.rb +++ b/test/support/operations.rb @@ -32,6 +32,18 @@ def apply_escaped_name(entity, _attribute, value) entity.where 'lower(name) LIKE ?', "%#{value.downcase}%" end end + + class FinderInherited < FinderWithEntity + property :raw_price, type: Types::Float, sortable: true + filter_by :price, with: :apply_price + paginator 'Kaminari' + + def apply_price(entity, _attribute, value) + return if value.blank? + + entity.where 'price < ?', value + end + end end module Product::Operations diff --git a/test/trailblazer/finder/dsl_spec.rb b/test/trailblazer/finder/dsl_spec.rb new file mode 100644 index 0000000..fd56ec6 --- /dev/null +++ b/test/trailblazer/finder/dsl_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'support/operations' + +module Trailblazer + class Finder + class DslSpec < Minitest::TrailblazerSpec + def define_finder_class(&block) + Class.new(Trailblazer::Finder) do + class_eval(&block) + end + end + + def finder_class(default_entity = [], &block) + define_finder_class do + entity { default_entity } + + class_eval(&block) unless block.nil? + end + end + + def new_finder(default_entity = [], filter = {}, &block) + finder_class(default_entity, &block).new params: filter + end + + describe '#adapters' do + it 'checks for a valid adapter and returns an error' do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity, value_eq: 'Test 1' do + adapter :NonExisting + end + + # expect(finder.errors).to eq [{adapter: "The specified adapter are invalid"}] + assert_equal finder.errors, [{ adapter: 'The specified adapter are invalid' }] + end + + it 'sets the adapter in the config' do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity, value_eq: 'Test 1' do + adapter 'ActiveRecord' + property :value, type: Base + end + + assert_equal finder.class.config.adapter, 'ActiveRecord' + end + end + + describe '#property' do + it 'checks for a valid property type and returns an error' do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity, value_eq: 'Test 1' do + property :value, type: Base + end + + assert_equal finder.errors, [{ properties: 'One or more properties are missing a valid type' }] + end + + it "sets the property and it's type properly in the config" do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity, value_eq: 'Test 1' do + property :value, type: Base + end + + assert_equal finder.class.config.properties, value: { type: Trailblazer::Finder::Base } + end + end + + describe '#paging' do + it 'sets the paging values' do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity do + paging per_page: 2, min_per_page: 1, max_per_page: 5 + property :value, type: Types::String + end + + assert_equal finder.class.config.paging[:per_page], 2 + assert_equal finder.class.config.paging[:min_per_page], 1 + assert_equal finder.class.config.paging[:max_per_page], 5 + end + + it "does not load paging stuff if paging isn't called in the finder class" do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity do + property :value, type: Types::String + end + + assert_empty finder.class.config.paging + end + end + + describe '#filter_by' do + it 'returns an error when supplied with a non symbol' do + entity = [1, 2, 3] + finder = new_finder entity, value_test: 'some' do + filter_by :value_test, with: 'test' + end + + assert_equal finder.result, errors: [{ filters: 'One or more filters are missing a with method definition' }] + end + + it 'has a default filter' do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity, value: 'Test 1' do + filter_by :value + end + + assert_equal finder.result.map { |n| n[:value] }, ['Test 1'] + end + + it "has a default filter working when it's nested" do + entity = [{ id: 1, value: [id: 4, value: 'Test 1'] }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity, value: 'Test 1' do + filter_by :value + end + + assert_equal finder.result.map { |n| n[:value] }, ['Test 1'] + assert_equal finder.result.map { |n| n[:id] }, [4] + end + + it 'has another default filter' do + entity = [{ id: 1, value: 'Test 1' }, { id: 2, value: 'Test 2' }, { id: 3, value: 'Test 3' }] + finder = new_finder entity, id: 2 do + filter_by :id + end + + assert_equal finder.result.map { |n| n[:value] }, ['Test 2'] + end + + it 'returns the entity if nil is returned' do + entity = [1, 2, 3] + finder = new_finder entity, value_test: 'some' do + filter_by :value_test do + nil + end + end + + assert_equal finder.result, entity + end + + it 'can use methods from the object' do + finder1 = new_finder [1, 2, 3], filter: 1 do + filter_by :filter do |entity, attribute, value| + some_instance_method(entity, attribute, value) + end + + private + + def some_instance_method(entity, _attribute, _value) + entity - [2, 3] + end + end + + assert_equal finder1.result, [1] + end + + it 'can dispatch with instance methods' do + finder = new_finder [1, 2, 3], filter: 1 do + filter_by :filter, with: :some_instance_method + + private + + def some_instance_method(entity, _attribute, value) + entity.select { |v| v == value } + end + end + + assert_equal finder.result, [1] + end + end + end + end +end diff --git a/test/trailblazer/finder/dsl_test.rb b/test/trailblazer/finder/dsl_test.rb index bb77b91..e3aea5f 100644 --- a/test/trailblazer/finder/dsl_test.rb +++ b/test/trailblazer/finder/dsl_test.rb @@ -42,7 +42,7 @@ def new_finder(default_entity = [], filter = {}, &block) property :value, type: Base end - assert_equal finder.class.config[:adapter], 'ActiveRecord' + assert_equal finder.class.config.adapter, 'ActiveRecord' end end @@ -62,7 +62,7 @@ def new_finder(default_entity = [], filter = {}, &block) property :value, type: Base end - assert_equal finder.class.config[:properties], value: { type: Trailblazer::Finder::Base } + assert_equal finder.class.config.properties, value: { type: Trailblazer::Finder::Base } end end @@ -74,9 +74,9 @@ def new_finder(default_entity = [], filter = {}, &block) property :value, type: Types::String end - assert_equal finder.class.config[:paging][:per_page], 2 - assert_equal finder.class.config[:paging][:min_per_page], 1 - assert_equal finder.class.config[:paging][:max_per_page], 5 + assert_equal finder.class.config.paging[:per_page], 2 + assert_equal finder.class.config.paging[:min_per_page], 1 + assert_equal finder.class.config.paging[:max_per_page], 5 end it "does not load paging stuff if paging isn't called in the finder class" do @@ -85,7 +85,7 @@ def new_finder(default_entity = [], filter = {}, &block) property :value, type: Types::String end - assert_empty finder.class.config[:paging] + assert_empty finder.class.config.paging end end diff --git a/test/trailblazer/operation/finder_spec.rb b/test/trailblazer/operation/finder_spec.rb index 61a6fed..81dad01 100644 --- a/test/trailblazer/operation/finder_spec.rb +++ b/test/trailblazer/operation/finder_spec.rb @@ -4,7 +4,7 @@ require 'support/operations' module Trailblazer - class Operation::FinderTest < Minitest::TrailblazerSpec + class Operation::FinderSpec < Minitest::TrailblazerSpec before do Product.destroy_all Product.reset_pk_sequence @@ -72,5 +72,19 @@ class Operation::FinderTest < Minitest::TrailblazerSpec assert_equal result[:finder].result.count, 11 assert_equal result[:finder].result.last.name, 'product_19' end + + + it 'can inherit finders' do + assert_equal Product::Finders::FinderWithEntity.current_adapter, 'ActiveRecord' + assert_nil Product::Finders::FinderWithEntity.current_paginator + ## the parent class has 1 filter and 2 properties and not being overwritten by the child class + assert_equal Product::Finders::FinderWithEntity.properties_count, 2 + assert_equal Product::Finders::FinderWithEntity.filters_count, 1 + + assert_equal Product::Finders::FinderInherited.current_adapter, 'ActiveRecord' + assert_equal Product::Finders::FinderInherited.current_paginator, 'Kaminari' + assert_equal Product::Finders::FinderInherited.properties_count, 3 + assert_equal Product::Finders::FinderInherited.filters_count, 2 + end end end