parascope 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 20fc5771d922f27cb3562954c138dd4d9069f01a
4
- data.tar.gz: 2da5ec9511b1cdea10f7a7e84aaa6023c358733c
3
+ metadata.gz: d24fcd353f9b0a8dc2091b654e35c4f6b3f973c4
4
+ data.tar.gz: 23a9825594b4ab16faa6deb34b4da2b5232fdd8c
5
5
  SHA512:
6
- metadata.gz: af2fce7745657960a666ae9d160b6fced85be97a3698f8b6cd1ed0d8f5b14d694e349222c011752071501e77f86a8ca62b415c2c22c4e5d75d3fc5648460a571
7
- data.tar.gz: ab35b12aa2b8a822dd53eb113867b63fc6cd1c00685021108273c724e4569dd78267b58b4d8da395a5c7c3db8157b951a91a6efc7d4e8b2f2636bccd5c732533
6
+ metadata.gz: c27284dc7a29103d72502a5db3232eca92fb99341df633e0f77c7c905a269e85bdc75ecf4a45e8a5945b94e0dbb72a289aa76fe7b24f41341d64421fec547e44
7
+ data.tar.gz: f5ae369d56e2ae924a358e950e8196127c1c31bc8a9c13d833fe479a6dcf36489b678502f3cebfee398f04d5281625afbef2c92ace9b24e2f37cafacb35fce73
data/README.md CHANGED
@@ -28,7 +28,7 @@ Or install it yourself as:
28
28
 
29
29
  ## Usage
30
30
 
31
- Despite the fact `parsacope` was intended to help building ActiveRecord relations
31
+ Despite the fact `parascope` was intended to help building ActiveRecord relations
32
32
  via scopes or query methods, it's usage is not limited to ActiveRecord cases and
33
33
  may be used with any arbitrary classes and objects. In fact, the only gem's dependency
34
34
  is `hashie`, and for development and testing, `OpenStruct` instance is used as a
@@ -44,12 +44,21 @@ scope manipulations using `query_by`, `sift_by` and other class methods bellow.
44
44
 
45
45
  - `query_by(*presence_fields, **value_fields, &block)` declares a scope-generation query
46
46
  block that will be executed if, and only if all values of query params at the keys of
47
- `presence_fields` are present in activesupport's definition of presence and all value
48
- fields are present in query params as is. The block is executed in context of query
47
+ `presence_fields` are present in activesupport's definition of presence and all `value_fields`
48
+ are present in query params as is. The block is executed in context of query
49
49
  object. All values of specified params are yielded to the block. If the block
50
- returns a non-nil value, it becomes a new scope for following processing. Of course,
51
- there can be multiple `query_by` block definitions. Optionally, `:index` option
52
- may be passed to control query blocks application order.
50
+ returns a non-nil value, it becomes a new scope for subsequent processing. Of course,
51
+ there can be multiple `query_by` block definitions. Methods accepts additional options:
52
+ - `:index` - allows to specify order of query block applications. By default all query
53
+ blocks have index of 0;
54
+ - `:if` - specifies condition according to which query should be applied. If Symbol
55
+ or String is passed, calls corresponding method. If Proc is passed, it is executed
56
+ in context of query object. Note that this is optional condition, and does not
57
+ overwrite original param-based condition for a query block that should always be met.
58
+ - `:unless` - the same as `:if` option, but with reversed boolean check.
59
+
60
+ - `query(&block)` declares scope-generation block that is always executed. As `query_by`,
61
+ accepts `:index`, `:if` and `:unless` options.
53
62
 
54
63
  - `sift_by(*presence_fields, **value_fields, &block)` method is used to hoist sets of
55
64
  query definitions that should be applied if, and only if, all specified values
@@ -57,6 +66,9 @@ scope manipulations using `query_by`, `sift_by` and other class methods bellow.
57
66
  values of specified fields are yielded to the block. Such `sift_by` definitions
58
67
  may be nested in any depth.
59
68
 
69
+ - `sifter` alias for `sift_by`. Results in a more readable construct when a single
70
+ presence field is passed. For example, `sifter(:paginated)`.
71
+
60
72
  - `base_scope(&block)` method is used to define a base scope as a starting point
61
73
  of scope-generating process. If this method is called from `sift_by` block,
62
74
  top-level base scope is yielded to the method block. Note that `base_scope` will
@@ -86,14 +98,16 @@ scope manipulations using `query_by`, `sift_by` and other class methods bellow.
86
98
  it's mutated version corresponding to passed `query_by` arguments.
87
99
 
88
100
  - `guard(&block)` executes a passed `block`. If this execution returns falsy value,
89
- `UnpermittedError` is raised. You can use this method to ensure safety of param
101
+ `GuardViolationError` is raised. You can use this method to ensure safety of param
90
102
  values interpolation to a SQL string in a `query_by` block for example.
91
103
 
92
- - `resolved_scope(override_params = nil)` returns a resulting scope generated by
93
- all queries and sifted queries that fit to query params applied to base scope.
94
- Optionally, additional params may be passed to override the ones passed on
95
- initialization. It's the main `Query` instance method that returns the sole
96
- purpose of it's instances.
104
+ - `resolved_scope(*presence_keys, override_params = {})` returns a resulting scope
105
+ generated by all queries and sifted queries that fit to query params applied to
106
+ base scope. Optionally, additional params may be passed to override the ones passed on
107
+ initialization. For convinience, you may pass list of keys that should be resolved
108
+ to `true` with params (for example, `resolved_scope(:with_projects)` instead of
109
+ `resolved_scope(with_projects: true)`). It's the main `Query` instance method that
110
+ returns the sole purpose of it's instances.
97
111
 
98
112
  ### Usage example with ActiveRecord Relation as a scope
99
113
 
@@ -120,12 +134,12 @@ class UserQuery < Parascope::Query
120
134
 
121
135
  base_scope { |scope| scope.order(scol => sdir) }
122
136
 
123
- query_by(:sort_direction, sort_column: 'name') do |sdir|
137
+ query_by(sort_column: 'name') do
124
138
  scope.reorder("CONCAT(first_name, ' ', last_name) #{sdir}")
125
139
  end
126
140
  end
127
141
 
128
- sift_by :with_projects do
142
+ sifter :with_projects do
129
143
  base_scope { |scope| scope.joins(:projects) }
130
144
 
131
145
  query_by :project_name do |name|
@@ -138,7 +152,7 @@ class UserQuery < Parascope::Query
138
152
  end
139
153
 
140
154
  def project_users
141
- @project_users ||= resolved_scope(with_projects: true)
155
+ @project_users ||= resolved_scope(:with_projects)
142
156
  end
143
157
  end
144
158
 
@@ -45,12 +45,23 @@ class Query < Parascope::Query
45
45
  end
46
46
  end
47
47
 
48
+ sift_by :other_baz do |other|
49
+ query { scope.tap{ scope.other_baz = other } }
50
+ end
51
+
52
+ query(if: :condition?) { scope.tap{ scope.by_instance_condition = true } }
53
+ query(unless: :bad_condition?) { scope.tap{ scope.not_bad_condition = true } }
54
+
55
+ def condition?
56
+ !!params[:condition]
57
+ end
58
+
48
59
  def upcase(str)
49
60
  str.upcase
50
61
  end
51
62
  end
52
63
 
53
- q = Query.new(foo: 'foo', bar: 'bar', baz: 'baz', bak: 'bak', nested_baz: 'nb')
64
+ q = Query.new(foo: 'foo', bar: 'bar', baz: 'baz', bak: 'bak', nested_baz: 'nb', other_baz: 'ob')
54
65
 
55
66
  require "pry"
56
67
  Pry.start
@@ -9,7 +9,9 @@ module Parascope
9
9
  extend ApiMethods
10
10
 
11
11
  UndefinedScopeError = Class.new(StandardError)
12
- UnpermittedError = Class.new(ArgumentError)
12
+ GuardViolationError = Class.new(ArgumentError)
13
+ # for backward-compatability
14
+ UnpermittedError = GuardViolationError
13
15
 
14
16
  attr_reader :params
15
17
  def_delegator :params, :[]
@@ -25,7 +27,8 @@ module Parascope
25
27
  def initialize(params, scope: nil, **attrs)
26
28
  @params = Hashie::Mash.new(klass.defaults).merge(params || {})
27
29
  @scope = scope unless scope.nil?
28
- @attrs = attrs
30
+ @attrs = attrs.freeze
31
+ @base_params = @params
29
32
  define_attr_readers
30
33
  end
31
34
 
@@ -48,10 +51,11 @@ module Parascope
48
51
  scope
49
52
  end
50
53
 
51
- def resolved_scope(params = nil)
52
- return sifted_instance.resolved_scope! if params.nil?
54
+ def resolved_scope(*args)
55
+ arg_params = args.pop if args.last.is_a?(Hash)
56
+ return sifted_instance.resolved_scope! if arg_params.nil? && args.empty?
53
57
 
54
- clone_with_params(params).resolved_scope
58
+ clone_with_params(trues(args).merge(arg_params || {})).resolved_scope
55
59
  end
56
60
 
57
61
  def klass
@@ -65,9 +69,9 @@ module Parascope
65
69
  attr_reader :attrs
66
70
 
67
71
  def sifted_instance
68
- block = klass.sift_blocks.find{ |block| block.fits?(params) }
72
+ blocks = klass.sift_blocks.select{ |block| block.fits?(self) }
69
73
 
70
- block ? sifted_instance_for(block) : self
74
+ blocks.size > 0 ? sifted_instance_for(blocks) : self
71
75
  end
72
76
 
73
77
  def resolved_scope!
@@ -78,37 +82,31 @@ module Parascope
78
82
  end
79
83
 
80
84
  def apply_block!
81
- if block && block.fits?(params)
85
+ if block && block.fits?(self)
82
86
  scope = instance_exec(*block.values_for(params), &block.block)
83
87
  @scope = scope unless scope.nil?
84
88
  end
85
89
  self
86
90
  end
87
91
 
88
- def sifted!(block, query)
92
+ def sifted!(query, blocks)
89
93
  @attrs = query.attrs
90
94
  define_attr_readers
91
95
  singleton_class.query_blocks.replace query.klass.query_blocks.dup
92
96
  singleton_class.guard_blocks.replace query.klass.guard_blocks.dup
93
97
  singleton_class.base_scope(&query.klass.base_scope)
94
- singleton_class.instance_exec(*block.values_for(params), &block.block)
98
+ blocks.each do |block|
99
+ singleton_class.instance_exec(*block.values_for(params), &block.block)
100
+ end
95
101
  params.replace(singleton_class.defaults.merge(params))
96
102
  @sifted = true
97
103
  end
98
104
 
99
- private
100
-
101
- def guard_all
102
- klass.guard_blocks.each{ |block| guard(&block) }
103
- end
104
-
105
- def guard(&block)
106
- unless instance_exec(&block)
107
- fail UnpermittedError, "processing is not allowed by guard block\non #{block.source_location.join(':')}"
108
- end
105
+ def sifted?
106
+ !!@sifted
109
107
  end
110
108
 
111
- def clone_with_scope(scope, block)
109
+ def clone_with_scope(scope, block = nil)
112
110
  clone.tap do |query|
113
111
  query.scope = scope
114
112
  query.block = block
@@ -116,28 +114,45 @@ module Parascope
116
114
  end
117
115
 
118
116
  def clone_with_params(other_params)
119
- clone.tap do |query|
120
- query.params = params.merge(other_params)
117
+ dup.tap do |query|
118
+ query.params = @base_params.merge(other_params)
119
+ query.remove_instance_variable('@sifted') if query.instance_variable_defined?('@sifted')
120
+ query.remove_instance_variable('@scope') if query.instance_variable_defined?('@scope')
121
+ query.define_attr_readers
121
122
  end
122
123
  end
123
124
 
124
- def clone_with_sifter(block)
125
+ def clone_sifted_with(blocks)
125
126
  dup.tap do |query|
126
- query.sifted!(block, self)
127
+ query.sifted!(self, blocks)
127
128
  end
128
129
  end
129
130
 
130
- def sifted?
131
- !!@sifted
131
+ def define_attr_readers
132
+ @attrs.each do |name, value|
133
+ define_singleton_method(name){ value }
134
+ end
132
135
  end
133
136
 
134
- def sifted_instance_for(block)
135
- clone_with_sifter(block).sifted_instance
137
+ private
138
+
139
+ def guard_all
140
+ klass.guard_blocks.each{ |block| guard(&block) }
136
141
  end
137
142
 
138
- def define_attr_readers
139
- @attrs.each do |name, value|
140
- define_singleton_method(name){ value }
143
+ def guard(&block)
144
+ unless instance_exec(&block)
145
+ fail GuardViolationError, "guard block violated on #{block.source_location.join(':')}"
146
+ end
147
+ end
148
+
149
+ def sifted_instance_for(blocks)
150
+ clone_sifted_with(blocks).sifted_instance
151
+ end
152
+
153
+ def trues(keys)
154
+ keys.each_with_object({}) do |key, hash|
155
+ hash[key] = true
141
156
  end
142
157
  end
143
158
  end
@@ -1,7 +1,22 @@
1
1
  module Parascope
2
- class Query::ApiBlock < Struct.new(:presence_fields, :value_fields, :block, :index)
3
- def fits?(params)
4
- values_for(params).all?{ |value| present?(value) }
2
+ class Query::ApiBlock
3
+ OPTION_KEYS = %i[index if unless].freeze
4
+ private_constant :OPTION_KEYS
5
+
6
+ attr_reader :presence_fields, :value_fields, :block, :options
7
+
8
+ def initialize(presence_fields:, value_fields:, block:)
9
+ @options = extract_options!(value_fields)
10
+
11
+ @presence_fields, @value_fields, @block =
12
+ presence_fields, value_fields, block
13
+ end
14
+
15
+ def fits?(query)
16
+ return false unless conditions_met_by?(query)
17
+
18
+ (presence_fields.size == 0 && value_fields.size == 0) ||
19
+ values_for(query.params).all?{ |value| present?(value) }
5
20
  end
6
21
 
7
22
  def values_for(params)
@@ -12,12 +27,41 @@ module Parascope
12
27
  value.respond_to?(:empty?) ? !value.empty? : !!value
13
28
  end
14
29
 
30
+ def index
31
+ options[:index] || 0
32
+ end
33
+
15
34
  private
16
35
 
36
+ def extract_options!(fields)
37
+ fields.keys.each_with_object({}) do |key, options|
38
+ options[key] = fields.delete(key) if OPTION_KEYS.include?(key)
39
+ end
40
+ end
41
+
17
42
  def valued_values_for(params)
18
43
  value_fields.map do |field, required_value|
19
44
  params[field] == required_value && required_value
20
45
  end
21
46
  end
47
+
48
+ def conditions_met_by?(query)
49
+ condition_met?(query, :if) && condition_met?(query, :unless)
50
+ end
51
+
52
+ def condition_met?(query, key)
53
+ return true unless options.key?(key)
54
+
55
+ condition = options[key]
56
+
57
+ value =
58
+ case condition
59
+ when String, Symbol then query.send(condition)
60
+ when Proc then query.instance_exec(&condition)
61
+ else condition
62
+ end
63
+
64
+ key == :if ? value : !value
65
+ end
22
66
  end
23
67
  end
@@ -15,13 +15,24 @@ module Parascope
15
15
  end
16
16
 
17
17
  def sift_by(*presence_fields, **value_fields, &block)
18
- sift_blocks.push Query::ApiBlock.new(presence_fields, value_fields, block)
18
+ sift_blocks.push Query::ApiBlock.new(
19
+ presence_fields: presence_fields,
20
+ value_fields: value_fields,
21
+ block: block
22
+ )
19
23
  end
20
24
 
21
- def query_by(*presence_fields, index: 0, **value_fields, &block)
22
- query_blocks.push Query::ApiBlock.new(presence_fields, value_fields, block, index)
25
+ def query_by(*presence_fields, **value_fields, &block)
26
+ query_blocks.push Query::ApiBlock.new(
27
+ presence_fields: presence_fields,
28
+ value_fields: value_fields,
29
+ block: block
30
+ )
23
31
  end
24
32
 
33
+ alias_method :sifter, :sift_by
34
+ alias_method :query, :query_by
35
+
25
36
  def guard(&block)
26
37
  guard_blocks.push block
27
38
  end
@@ -1,3 +1,3 @@
1
1
  module Parascope
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parascope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Kuzko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-05-09 00:00:00.000000000 Z
11
+ date: 2016-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie