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 +4 -4
- data/README.md +29 -15
- data/bin/console +12 -1
- data/lib/parascope/query.rb +47 -32
- data/lib/parascope/query/api_block.rb +47 -3
- data/lib/parascope/query/api_methods.rb +14 -3
- data/lib/parascope/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d24fcd353f9b0a8dc2091b654e35c4f6b3f973c4
|
4
|
+
data.tar.gz: 23a9825594b4ab16faa6deb34b4da2b5232fdd8c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 `
|
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
|
48
|
-
|
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
|
51
|
-
there can be multiple `query_by` block definitions.
|
52
|
-
|
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
|
-
`
|
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 =
|
93
|
-
all queries and sifted queries that fit to query params applied to
|
94
|
-
Optionally, additional params may be passed to override the ones passed on
|
95
|
-
initialization.
|
96
|
-
|
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(
|
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
|
-
|
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
|
155
|
+
@project_users ||= resolved_scope(:with_projects)
|
142
156
|
end
|
143
157
|
end
|
144
158
|
|
data/bin/console
CHANGED
@@ -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
|
data/lib/parascope/query.rb
CHANGED
@@ -9,7 +9,9 @@ module Parascope
|
|
9
9
|
extend ApiMethods
|
10
10
|
|
11
11
|
UndefinedScopeError = Class.new(StandardError)
|
12
|
-
|
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(
|
52
|
-
|
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(
|
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
|
-
|
72
|
+
blocks = klass.sift_blocks.select{ |block| block.fits?(self) }
|
69
73
|
|
70
|
-
|
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?(
|
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!(
|
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
|
-
|
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
|
-
|
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
|
-
|
120
|
-
query.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
|
125
|
+
def clone_sifted_with(blocks)
|
125
126
|
dup.tap do |query|
|
126
|
-
query.sifted!(
|
127
|
+
query.sifted!(self, blocks)
|
127
128
|
end
|
128
129
|
end
|
129
130
|
|
130
|
-
def
|
131
|
-
|
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
|
-
|
135
|
-
|
137
|
+
private
|
138
|
+
|
139
|
+
def guard_all
|
140
|
+
klass.guard_blocks.each{ |block| guard(&block) }
|
136
141
|
end
|
137
142
|
|
138
|
-
def
|
139
|
-
|
140
|
-
|
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
|
3
|
-
|
4
|
-
|
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(
|
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,
|
22
|
-
query_blocks.push Query::ApiBlock.new(
|
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
|
data/lib/parascope/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2016-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashie
|