qreds 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 22a68415c21985b6d9e24fd19cf1262f4590e3f1
4
+ data.tar.gz: 2cf842c2363fd974fb997de1c7488a9181d15d23
5
+ SHA512:
6
+ metadata.gz: bdb40b713c60d70e280301c145c4e855630b8c0c03114179d39efc319c52a580aebaa3c9115230c0aa72f01b5d1e05971e078cd9c01abcaf11aea74f2bfd3d1f
7
+ data.tar.gz: c44b8659d0dbc8063b1a6f2ba1cfd3f2c1600c78dc6df0aa6b62b846c16473285d0d5b4ef9d02a2b7a5d10e07f83c2b0723acff27e630bb9c0873f06119963a0
@@ -0,0 +1,12 @@
1
+ module Qreds
2
+ require 'bundler/setup'
3
+
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+
7
+ require_relative 'qreds/config'
8
+ require_relative 'qreds/functor'
9
+ require_relative 'qreds/catch_all_functor'
10
+ require_relative 'qreds/reducer'
11
+ require_relative 'qreds/endpoint'
12
+ end
@@ -0,0 +1,36 @@
1
+ module Qreds
2
+ class CatchAllFunctor < Functor
3
+ # @param query [any] the query to adjust
4
+ # @param value [any] the parameter value
5
+ # @param context [any]
6
+ # @param key [String] the parameter name
7
+ # @param config [Qreds::Config] current reducer config
8
+ def initialize(query, value, context, key, config)
9
+ super(query, value, context)
10
+
11
+ @key = key
12
+ @config = config
13
+ end
14
+
15
+ def call
16
+ head, _, tail = key.rpartition('_')
17
+ operator = map_operator(tail)
18
+ attr_name = operator.nil? ? key : head
19
+
20
+ config.default_lambda.call(query, attr_name, value, operator, context)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :key, :config
26
+
27
+ def map_operator(operator)
28
+ return nil if config.operator_mapping.nil?
29
+ mapped = config.operator_mapping[operator]
30
+
31
+ raise "No operator mapping found for #{operator}" if mapped.nil?
32
+
33
+ mapped
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,80 @@
1
+ require 'qreds/endpoint'
2
+
3
+ module Qreds
4
+ class Config
5
+ attr_accessor :functor_group, :default_lambda, :operator_mapping
6
+
7
+ @reducers = {}
8
+
9
+ # @param args [Hash<#to_s, any>]
10
+ def initialize(args)
11
+ args.each do |(key, value)|
12
+ send("#{key}=", value)
13
+ end
14
+ end
15
+
16
+ class << self
17
+ delegate :[], to: :@reducers
18
+
19
+ # @param helper_name [Symbol|String] the name of the helper method to be defined
20
+ # @yield config [Hash]
21
+ def define_reducer(helper_name, strategy: method(:define_endpoint_method))
22
+ config = new(functor_group: helper_name)
23
+
24
+ yield config
25
+
26
+ strategy.call(helper_name, config)
27
+ end
28
+
29
+ private
30
+
31
+ def define_endpoint_method(helper_name, config)
32
+ ::Qreds::Endpoint.send(:define_method, helper_name) do |query, context={}, **args|
33
+ functor_group = config.functor_group
34
+
35
+ declared_params = declared(params, include_missing: false)[functor_group]
36
+
37
+ ::Qreds::Reducer.new(
38
+ query: query,
39
+ params: declared_params,
40
+ config: config,
41
+ context: context,
42
+ **args
43
+ ).call
44
+ end
45
+
46
+ @reducers[helper_name] = config
47
+ end
48
+ end
49
+
50
+ OPERATOR_MAPPING_COMP_PGSQL = {
51
+ 'lt' => '< ?',
52
+ 'lte' => '<= ?',
53
+ 'eq' => '= ?',
54
+ 'gt' => '> ?',
55
+ 'gte' => '>= ?',
56
+ 'in' => 'IN (?)',
57
+ 'btw' => 'BETWEEN ? AND ?'
58
+ }
59
+
60
+ private_constant :OPERATOR_MAPPING_COMP_PGSQL
61
+
62
+ define_reducer :sort do |reducer|
63
+ reducer.default_lambda = ->(query, attr_name, value, _, _) do
64
+ query.order(attr_name => value)
65
+ end
66
+ end
67
+
68
+ define_reducer :filter do |reducer|
69
+ reducer.default_lambda = ->(query, attr_name, value, operator, _) do
70
+ if operator.count('?') > 1
71
+ query.where("#{attr_name} #{operator}", *value)
72
+ else
73
+ query.where("#{attr_name} #{operator}", value)
74
+ end
75
+ end
76
+ reducer.operator_mapping = OPERATOR_MAPPING_COMP_PGSQL
77
+ reducer.functor_group = 'filters'
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,6 @@
1
+ module Qreds
2
+ # A container for helper methods to invoke reducers.
3
+ # The methods are defined via configuration (Config.define_reducer)
4
+ module Endpoint
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ module Qreds
2
+ class Functor
3
+ # @param query [any] the query to adjust
4
+ # @param value [any] the parameter value
5
+ # @param context [any]
6
+ def initialize(query, value, context={})
7
+ @query = query
8
+ @value = value
9
+ @context = context
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :query, :value, :context
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ module Qreds
2
+ class Reducer
3
+ # @param query [any] the query to be reduced
4
+ # @param params [Hash] with keys being functor names and values the functor arguments.
5
+ # @param config [Qreds::Config] current reducer config
6
+ # @param resource_name [String] the name of the resource that query operates on
7
+ # @param context [any]
8
+ def initialize(query:, params:, config:, resource_name: query.model.to_s, context: {})
9
+ @query = query
10
+ @params = params
11
+ @config = config
12
+ @resource_name = resource_name
13
+ @context = context
14
+ end
15
+
16
+ def call
17
+ return query if params.blank?
18
+
19
+ params.reduce(query) do |reduced_query, (functor_key, functor_value)|
20
+ functor_instance(functor_key, reduced_query, functor_value).call
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :query, :params, :config, :resource_name, :context
27
+
28
+ def functor_instance(functor_key, reduced_query, functor_value)
29
+ functor_group_name = config.functor_group.to_s.capitalize
30
+ functor_name = functor_key.classify
31
+
32
+ klass = functor_class(functor_key, reduced_query, functor_value)
33
+
34
+ return klass.new(reduced_query, functor_value, context) if klass
35
+
36
+ ::Qreds::CatchAllFunctor.new(
37
+ reduced_query,
38
+ functor_value,
39
+ context,
40
+ functor_key,
41
+ config
42
+ )
43
+ end
44
+
45
+ def functor_class(functor_key, reduced_query, functor_value)
46
+ functor_group_name = config.functor_group.to_s.capitalize
47
+ functor_name = functor_key.classify
48
+
49
+ klass = "::#{functor_group_name}::#{resource_name}::#{functor_name}".constantize
50
+ rescue NameError
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Qreds::CatchAllFunctor do
4
+ subject { described_class.new(query, value, context, key, config).call.map(&:value) }
5
+
6
+ let(:query) { MockCollection.new((1..3).map { |i| SimpleObject.new(i) }) }
7
+ let(:key) { 'some_field' }
8
+ let(:value) { 2 }
9
+ let(:context) { {} }
10
+ let(:base_config) do
11
+ {
12
+ default_lambda: ->(collection, attr_name, value, operator, context) do
13
+ operator = '==' if operator.nil?
14
+ collection.select { |x| x.public_send(attr_name).public_send(operator, value) }
15
+ end
16
+ }
17
+ end
18
+ let(:config) { Qreds::Config.new(base_config) }
19
+
20
+ it "calls given lambda and returns it's value" do
21
+ is_expected.to eq([2])
22
+ end
23
+
24
+ context 'when config has operator mapping' do
25
+ let(:config) do
26
+ Qreds::Config.new(
27
+ **base_config,
28
+ operator_mapping: {
29
+ 'gte' => '>='
30
+ }
31
+ )
32
+ end
33
+
34
+ it 'raises a RuntimeError' do
35
+ expect { subject }.to raise_error(RuntimeError)
36
+ end
37
+
38
+ context 'when operator is used as key suffix' do
39
+ let(:key) { 'some_field_gte' }
40
+
41
+ it { is_expected.to eq([2, 3]) }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Qreds::Config do
4
+ let(:query) { MockCollection.new((1..3).map { |i| SimpleObject.new(i) }) }
5
+ let(:attr_name) { 'some_field' }
6
+
7
+ describe '.define_reducer' do
8
+ let(:reducer) do
9
+ test_strategy = -> (_helper_name, config) { config }
10
+
11
+ described_class.define_reducer(:test_reducer, strategy: test_strategy) do |config|
12
+ config.operator_mapping = {}
13
+ end
14
+ end
15
+
16
+ it 'creates a reducer with defaults and allows changing any other keys' do
17
+ expect(reducer.functor_group).to eq(:test_reducer)
18
+ expect(reducer.operator_mapping).to eq({})
19
+ end
20
+ end
21
+
22
+ describe 'sorting reducer' do
23
+ let(:reducer) { described_class[:sort] }
24
+
25
+ describe 'default lambda' do
26
+ subject { reducer.default_lambda.call(query, attr_name, value, nil, {}).map(&:value) }
27
+ let(:value) { 'desc' }
28
+
29
+ it 'calls order with attr name and value' do
30
+ is_expected.to eq([3, 2, 1])
31
+ end
32
+ end
33
+ end
34
+
35
+ describe 'filtering reducer' do
36
+ let(:reducer) { described_class[:filter] }
37
+
38
+ describe 'default lambda' do
39
+ subject { reducer.default_lambda.call(query, attr_name, value, operator, {}).map(&:value) }
40
+ let(:value) { 2 }
41
+ let(:operator) { '>' }
42
+
43
+ it 'calls where with attr name, value and operator' do
44
+ is_expected.to eq([3])
45
+ end
46
+
47
+ context 'when translated operator has more than one "?"' do
48
+ let(:operator) { 'BETWEEN ? AND ?' }
49
+ let(:value) { [2, 3] }
50
+
51
+ it { is_expected.to eq([2, 3]) }
52
+ end
53
+ end
54
+
55
+ describe 'operator mapping' do
56
+ subject { reducer.operator_mapping }
57
+
58
+ it 'is suited to work with PostgreSQL' do
59
+ is_expected.to eq(
60
+ 'lt' => '< ?',
61
+ 'lte' => '<= ?',
62
+ 'eq' => '= ?',
63
+ 'gt' => '> ?',
64
+ 'gte' => '>= ?',
65
+ 'in' => 'IN (?)',
66
+ 'btw' => 'BETWEEN ? AND ?'
67
+ )
68
+ end
69
+ end
70
+
71
+ describe 'functor_group' do
72
+ subject { reducer.functor_group }
73
+
74
+ it { is_expected.to eq('filters') }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Qreds::Endpoint do
4
+ let(:endpoint) { MockEndpoint.new(params) }
5
+ let(:query) { MockCollection.new([2, 3, 1]) }
6
+
7
+ describe '#sort' do
8
+ subject { endpoint.sort(query) }
9
+ let(:params) do
10
+ {
11
+ 'sort' => {
12
+ 'simple' => 'asc'
13
+ }
14
+ }
15
+ end
16
+
17
+ it { is_expected.to eq([1, 2, 3]) }
18
+
19
+ context 'with dynamic sorting - no predefined sort' do
20
+ subject { endpoint.sort(query).map(&:value) }
21
+ let(:query) { MockCollection.new([2, 3, 1].map { |i| SimpleObject.new(i) })}
22
+ let(:params) do
23
+ {
24
+ 'sort' => {
25
+ 'some_field' => 'desc'
26
+ }
27
+ }
28
+ end
29
+
30
+ it { is_expected.to eq([3, 2, 1]) }
31
+ end
32
+ end
33
+
34
+ describe '#filter' do
35
+ subject { endpoint.filter(query) }
36
+ let(:params) do
37
+ {
38
+ 'filters' => {
39
+ 'equality' => 2
40
+ }
41
+ }
42
+ end
43
+
44
+ it 'filters the collection' do
45
+ is_expected.to eq([2])
46
+ end
47
+
48
+ context 'with dynamic filtering - no predefined filter' do
49
+ subject { endpoint.filter(query).map(&:value) }
50
+ let(:query) { MockCollection.new([1, 2, 3].map { |i| SimpleObject.new(i) })}
51
+ let(:params) do
52
+ {
53
+ 'filters' => {
54
+ 'some_field_eq' => 2
55
+ }
56
+ }
57
+ end
58
+
59
+ it { is_expected.to eq([2]) }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Qreds::Reducer do
4
+ let(:base_args) do
5
+ {
6
+ query: query,
7
+ params: params,
8
+ config: config
9
+ }
10
+ end
11
+ let(:args) { base_args }
12
+ let(:fallback_method) { nil }
13
+
14
+ let(:config) do
15
+ Qreds::Config.new(
16
+ default_lambda: ->(*_) { ['transformed'] },
17
+ functor_group: functor_group
18
+ )
19
+ end
20
+
21
+ subject { described_class.new(args).call }
22
+
23
+ let(:functor_group) { 'Filters' }
24
+ let(:query) { MockCollection.new([1, 2, 3]) }
25
+ let(:params) { { 'equality' => 1 } }
26
+
27
+ it 'calls specified functor' do
28
+ is_expected.to eq([1])
29
+ end
30
+
31
+ context 'when params is empty' do
32
+ let(:params) { {} }
33
+
34
+ it 'returns the collection unchanged' do
35
+ is_expected.to eq(query)
36
+ end
37
+ end
38
+
39
+ context 'when passed a different resource_name' do
40
+ let(:resource_name) { 'DifferentMockModel' }
41
+ let(:args) { base_args.merge(resource_name: resource_name) }
42
+
43
+ it 'calls the different resource' do
44
+ is_expected.to eq([2, 3])
45
+ end
46
+ end
47
+
48
+ context 'when cannot find a class' do
49
+ let(:query) { MockCollection.new((1..3).map { |i| SimpleObject.new(i) }) }
50
+ let(:params) { { 'some_field' => 2 } }
51
+
52
+ subject { described_class.new(args).call }
53
+
54
+ it 'uses the config default lambda' do
55
+ is_expected.to eq(['transformed'])
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,11 @@
1
+ require 'bundler/setup'
2
+ require 'qreds'
3
+
4
+ Bundler.require(:default, :test)
5
+
6
+ require_relative 'support/mock_collection'
7
+ require_relative 'support/mock_endpoint'
8
+ require_relative 'support/mock_model'
9
+ require_relative 'support/filters'
10
+ require_relative 'support/sort'
11
+ require_relative 'support/simple_object'
@@ -0,0 +1,17 @@
1
+ module Filters
2
+ module MockModel
3
+ class Equality < ::Qreds::Functor
4
+ def call
5
+ query.select { |el| el == value }
6
+ end
7
+ end
8
+ end
9
+
10
+ module DifferentMockModel
11
+ class Equality < ::Qreds::Functor
12
+ def call
13
+ query.select { |el| el != value }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ module TestBetweenInteger
2
+ def test_between_integer(left, right)
3
+ self >= left && self <= right
4
+ end
5
+ end
6
+
7
+ class Fixnum
8
+ include TestBetweenInteger
9
+ end
10
+
11
+ class Integer
12
+ include TestBetweenInteger
13
+ end
14
+
15
+ class MockCollection < Array
16
+ def model
17
+ MockModel
18
+ end
19
+
20
+ def where(arg, *values)
21
+ if values.nil?
22
+ handle_where_hash(arg)
23
+ else
24
+ handle_where_string(arg, *values)
25
+ end
26
+ end
27
+
28
+ def order(hash)
29
+ k, v = key_val(hash)
30
+
31
+ sort_by do |el|
32
+ sort_val = el.public_send(k)
33
+ v == 'asc' ? sort_val : -sort_val
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def handle_where_hash(hash)
40
+ k, v = key_val(main_arg)
41
+
42
+ select { |el| el.public_send(k) == v }
43
+ end
44
+
45
+ def handle_where_string(attr_name_with_operator, *values)
46
+ attr_name, _, operator = attr_name_with_operator.partition(' ')
47
+
48
+ select { |el| el.public_send(attr_name).public_send(sanitize_operator(operator), *values) }
49
+ end
50
+
51
+ def sanitize_operator(operator)
52
+ return :== if operator == '= ?'
53
+ return :test_between_integer if operator == 'BETWEEN ? AND ?'
54
+ operator
55
+ end
56
+
57
+ def key_val(hash)
58
+ k = hash.keys.first
59
+ v = hash[k]
60
+
61
+ [k, v]
62
+ end
63
+ end
@@ -0,0 +1,13 @@
1
+ class MockEndpoint
2
+ include Qreds::Endpoint
3
+
4
+ def initialize(params)
5
+ @params = params
6
+ end
7
+
8
+ def declared(params, **_)
9
+ params.with_indifferent_access
10
+ end
11
+
12
+ attr_reader :params
13
+ end
@@ -0,0 +1,2 @@
1
+ class MockModel
2
+ end
@@ -0,0 +1,11 @@
1
+ class SimpleObject
2
+ def initialize(value)
3
+ @value = value
4
+ end
5
+
6
+ def some_field
7
+ @value
8
+ end
9
+
10
+ attr_reader :value
11
+ end
@@ -0,0 +1,9 @@
1
+ module Sort
2
+ module MockModel
3
+ class Simple < ::Qreds::Functor
4
+ def call
5
+ query.sort_by { |el| value == 'asc' ? el : -el }
6
+ end
7
+ end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qreds
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Rafał Warzocha
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Query reducers with built-in support for ActiveRecord & Grape, open for
42
+ custom extensions.
43
+ email: warzocha.rafal@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/qreds.rb
49
+ - lib/qreds/catch_all_functor.rb
50
+ - lib/qreds/config.rb
51
+ - lib/qreds/endpoint.rb
52
+ - lib/qreds/functor.rb
53
+ - lib/qreds/reducer.rb
54
+ - spec/qreds/catch_all_functor_spec.rb
55
+ - spec/qreds/config_spec.rb
56
+ - spec/qreds/endpoint_spec.rb
57
+ - spec/qreds/reducer_spec.rb
58
+ - spec/spec_helper.rb
59
+ - spec/support/filters.rb
60
+ - spec/support/mock_collection.rb
61
+ - spec/support/mock_endpoint.rb
62
+ - spec/support/mock_model.rb
63
+ - spec/support/simple_object.rb
64
+ - spec/support/sort.rb
65
+ homepage: https://github.com/rafal42/qreds
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.6.14
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Query reducers.
89
+ test_files:
90
+ - spec/spec_helper.rb
91
+ - spec/support/mock_collection.rb
92
+ - spec/support/filters.rb
93
+ - spec/support/mock_model.rb
94
+ - spec/support/mock_endpoint.rb
95
+ - spec/support/simple_object.rb
96
+ - spec/support/sort.rb
97
+ - spec/qreds/config_spec.rb
98
+ - spec/qreds/endpoint_spec.rb
99
+ - spec/qreds/catch_all_functor_spec.rb
100
+ - spec/qreds/reducer_spec.rb