appfuel 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/appfuel.gemspec +1 -1
  4. data/lib/appfuel/application/app_container.rb +0 -1
  5. data/lib/appfuel/application/dispatcher.rb +32 -0
  6. data/lib/appfuel/application/root.rb +4 -4
  7. data/lib/appfuel/application.rb +2 -1
  8. data/lib/appfuel/domain/domain_name_parser.rb +9 -5
  9. data/lib/appfuel/domain.rb +0 -7
  10. data/lib/appfuel/handler/base.rb +8 -14
  11. data/lib/appfuel/storage/db/mapper.rb +31 -35
  12. data/lib/appfuel/storage/db/repository.rb +53 -121
  13. data/lib/appfuel/storage/repository/base.rb +246 -0
  14. data/lib/appfuel/storage/repository/criteria.rb +317 -0
  15. data/lib/appfuel/{domain → storage/repository}/expr.rb +1 -1
  16. data/lib/appfuel/storage/repository/expr_conjunction.rb +41 -0
  17. data/lib/appfuel/{domain → storage/repository}/expr_parser.rb +10 -22
  18. data/lib/appfuel/{domain → storage/repository}/expr_transform.rb +7 -7
  19. data/lib/appfuel/{repository → storage/repository}/mapper.rb +8 -3
  20. data/lib/appfuel/storage/repository/order_expr.rb +51 -0
  21. data/lib/appfuel/storage/repository/runner.rb +62 -0
  22. data/lib/appfuel/storage/repository/search_parser.rb +50 -0
  23. data/lib/appfuel/storage/repository/search_transform.rb +58 -0
  24. data/lib/appfuel/{domain/criteria_settings.rb → storage/repository/settings.rb} +20 -5
  25. data/lib/appfuel/{repository.rb → storage/repository.rb} +13 -0
  26. data/lib/appfuel/storage.rb +1 -0
  27. data/lib/appfuel/version.rb +1 -1
  28. data/lib/appfuel.rb +0 -2
  29. metadata +21 -19
  30. data/lib/appfuel/domain/base_criteria.rb +0 -171
  31. data/lib/appfuel/domain/exists_criteria.rb +0 -57
  32. data/lib/appfuel/domain/expr_conjunction.rb +0 -27
  33. data/lib/appfuel/domain/search_criteria.rb +0 -137
  34. data/lib/appfuel/repository/base.rb +0 -86
  35. data/lib/appfuel/repository_runner.rb +0 -60
  36. /data/lib/appfuel/{repository → storage/repository}/initializer.rb +0 -0
  37. /data/lib/appfuel/{repository → storage/repository}/mapping_dsl.rb +0 -0
  38. /data/lib/appfuel/{repository → storage/repository}/mapping_entry.rb +0 -0
@@ -0,0 +1,246 @@
1
+ module Appfuel
2
+ module Repository
3
+ # The generic repository behavior. This represents repo behavior that is
4
+ # agnostic to any storage system. The following is a definition of this
5
+ # patter by Martin Fowler:
6
+ #
7
+ # "The repository mediates between the domain and data mapping
8
+ # layers using a collection-like interface for accessing domain
9
+ # objects."
10
+ #
11
+ # "Conceptually, a Repository encapsulates the set of objects persisted
12
+ # in a data store and the operations performed over them, providing a
13
+ # more object-oriented view of the persistence layer."
14
+ #
15
+ # https://martinfowler.com/eaaCatalog/repository.html
16
+ #
17
+ # While we are not a full repository pattern, we are evolving into it.
18
+ # All repositories have access to the application container. They register
19
+ # themselves into the container, as well as handling the cache from the
20
+ # container.
21
+ class Base
22
+ include Appfuel::Application::AppContainer
23
+
24
+ class << self
25
+ attr_writer :mapper
26
+
27
+ # Used when the concrete class is being registered, to construct
28
+ # the container key as a path.
29
+ #
30
+ # @example features.membership.repositories.user
31
+ # @example global.repositories.user
32
+ # @example <feature|global>.<container_class_type>.<class|container_key>
33
+ #
34
+ # @return [String]
35
+ def container_class_type
36
+ 'repositories'
37
+ end
38
+
39
+ # Stage the concrete class that is inheriting this for registration.
40
+ # The reason we have to stage the registration is to give the code
41
+ # enough time to mixin the AppContainer functionality needed for
42
+ # registration. Therefore registration is defered until feature
43
+ # initialization.
44
+ #
45
+ # @param klass [Class] the class inheriting this
46
+ # @return nil
47
+ def inherited(klass)
48
+ stage_class_for_registration(klass)
49
+ nil
50
+ end
51
+
52
+ # Mapper holds specific knowledge of storage to domain mappings
53
+ #
54
+ # @return [Mapper]
55
+ def mapper
56
+ @mapper ||= create_mapper
57
+ end
58
+
59
+ # Factory method to create a mapper. Each concrete Repository will
60
+ # override this.
61
+ #
62
+ # @param maps [Hash] the domain to storage mappings
63
+ # @return [Mapper]
64
+ def create_mapper(maps = nil)
65
+ Mapper.new(container_root_name, maps)
66
+ end
67
+
68
+ # A cache of already resolved domain objects
69
+ #
70
+ # @return [Hash]
71
+ def cache
72
+ app_container[:repository_cache]
73
+ end
74
+ end
75
+
76
+
77
+ # @return [Mapper]
78
+ def mapper
79
+ self.class.mapper
80
+ end
81
+
82
+ # Validate the method exists and call it with the criteria and
83
+ # settings objects
84
+ #
85
+ # @params query_method [String] method to call
86
+ # @params criteria [SearchCriteria]
87
+ # @params settings [Settings]
88
+ # @return DomainCollection
89
+ def execute_query_method(query_method, criteria, settings)
90
+ unless respond_to?(query_method)
91
+ fail "Could not execute query method (#{query_method})"
92
+ end
93
+
94
+ public_send(query_method, criteria, settings)
95
+ end
96
+
97
+ # The first method called in the query life cycle. It setups up the
98
+ # query method used to return a query relation for the next method
99
+ # in the life cycle. This query method will return a query relation
100
+ # produced by the concrete repo for that domain. The relation is specific
101
+ # to the type of repo, a db repo will return an ActiveRecordRelation for
102
+ # example.
103
+ #
104
+ # @param criteria [SearchCriteria]
105
+ # @param settings [Settings]
106
+ # @return [Object] A query relation
107
+ def query_setup(criteria, settings)
108
+ query_method = "#{criteria.domain_basename}_query"
109
+ execute_query_method(query_method, criteria, settings)
110
+ end
111
+
112
+ def query(criteria, settings = {})
113
+ settings = create_settings(settings)
114
+ criteria = build_criteria(criteria, settings)
115
+
116
+ if settings.manual_query?
117
+ query_method = settings.manual_query
118
+ return execute_query_method(query_method, criteria, settings)
119
+ end
120
+
121
+ begin
122
+ result = query_setup(criteria, settings)
123
+ result = handle_query_conditions(criteria, result, settings)
124
+ build_domains(criteria, result, settings)
125
+ rescue => e
126
+ msg = "query failed for #{criteria.domain_name}: " +
127
+ "#{e.class} #{e.message}"
128
+ error = RuntimeError.new(msg)
129
+ error.set_backtrace(e.backtrace)
130
+ raise error
131
+ end
132
+ end
133
+
134
+ # Query conditions can only be applied by a specific type of repo, like
135
+ # a database or elastic search repo. Because of this we will fail if
136
+ # this is not implemented
137
+ #
138
+ # @param result [Object] some type of query relation
139
+ # @param criteria [SearchCriteria]
140
+ # @param settings [Settings]
141
+ # @return A query relation
142
+ def apply_query_conditions(_result, _criteria, _settings)
143
+ method_not_implemented_error
144
+ end
145
+
146
+ # Domain resolution can only be applied by specific repos. Because of
147
+ # this we fail if is not implmented
148
+ #
149
+ # @param result [Object] some type of query relation
150
+ # @param criteria [SearchCriteria]
151
+ # @param settings [Settings]
152
+ # @return A query relation
153
+ def build_domains(_result, _criteria, _settings)
154
+ method_not_implemented_error
155
+ end
156
+
157
+ # Factory method to create repo settings. This holds things like
158
+ # pagination details, parser classes etc..
159
+ #
160
+ # @params settings [Hash,Settings]
161
+ # @return Settings
162
+ def create_settings(settings = {})
163
+ return settings if settings.instance_of?(Settings)
164
+ Settings.new(settings)
165
+ end
166
+
167
+ def build_criteria(criteria, settings = nil)
168
+ settings ||= create_settings
169
+
170
+ return criteria if criteria?(criteria)
171
+
172
+ if criteria.is_a?(String)
173
+ tree = settings.parser.parse(criteria)
174
+ result = settings.transform.apply(tree)
175
+ return result[:search]
176
+ end
177
+
178
+ unless criteria.is_a?(Hash)
179
+ fail "criteria must be a String, Hash, or " +
180
+ "Appfuel::Domain::SearchCriteria"
181
+ end
182
+ Criteria.build(criteria)
183
+ end
184
+
185
+ def criteria?(value)
186
+ value.instance_of?(Criteria)
187
+ end
188
+
189
+ def exists?(criteria)
190
+ expr = criteria.fiilters
191
+ mapper.exists?(expr)
192
+ end
193
+
194
+ def to_storage(entity, exclude = [])
195
+ mapper.to_storage(entity, exclude)
196
+ end
197
+
198
+ def to_entity(domain_name, storage)
199
+ key = qualify_container_key(domain_name, "domains")
200
+ hash = mapper.to_entity_hash(domain_name, storage)
201
+ app_container[key].new(hash)
202
+ end
203
+
204
+ def build(type:, name:, storage:, **inputs)
205
+ builder = find_entity_builder(type: type, domain_name: name)
206
+ builder.call(storage, inputs)
207
+ end
208
+
209
+ # features.membership.presenters.hash.user
210
+ # global.presenters.user
211
+ #
212
+ # key => db_model
213
+ # key => db_model
214
+ def find_entity_builder(domain_name:, type:)
215
+ key = qualify_container_key(domain_name, "domain_builders.#{type}")
216
+
217
+ container = app_container
218
+ unless container.key?(key)
219
+ return ->(data, inputs = {}) {
220
+ build_default_entity(domain_name: domain_name, storage: data)
221
+ }
222
+ end
223
+
224
+ container[key]
225
+ end
226
+
227
+ def build_default_entity(domain_name:, storage:)
228
+ storage = [storage] unless storage.is_a?(Array)
229
+
230
+ storage_attrs = {}
231
+ storage.each do |model|
232
+ storage_attrs.merge!(mapper.model_attributes(model))
233
+ end
234
+
235
+ hash = mapper.to_entity_hash(domain_name, storage_attrs)
236
+ key = qualify_container_key(domain_name, "domains")
237
+ app_container[key].new(hash)
238
+ end
239
+
240
+ private
241
+ def method_not_implemented_error
242
+ fail "must be implemented by a storage specific repository"
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,317 @@
1
+ module Appfuel
2
+ module Repository
3
+
4
+ # The Criteria represents the interface between the repositories and actions
5
+ # or commands. The allow you to find entities in the application storage (
6
+ # a database) without knowledge of that storage system. The criteria will
7
+ # always refer to its queries in the domain language for which the repo is
8
+ # responsible for mapping that query to its persistence layer.
9
+ #
10
+ # global.user
11
+ # memberships.user
12
+ #
13
+ # exist: 'foo.bar exists id = 6'
14
+ # search: 'foo.bar filter id = 6 and bar = "foo" order id asc limit 6'
15
+ #
16
+ # search:
17
+ # domain: 'foo.bar',
18
+ #
19
+ # filters: 'id = 6 or id = 8 and id = 9'
20
+ # filters: [
21
+ # ['id', 'eq', '6', 'or']
22
+ # ]
23
+ # filters: [
24
+ # {attr: 'id', op: 'eq', value: 999, or: true},
25
+ # ]
26
+ #
27
+ # order: 'foo.bar.id asc'
28
+ # order: 'foo.bar.id'
29
+ # order: [
30
+ # 'foo.bar.id',
31
+ # {desc: 'foo.bar.id'},
32
+ # {asc: 'foo.bar.id'}
33
+ # ]
34
+ # limit: 1
35
+ #
36
+ class Criteria
37
+ include Appfuel::Domain::DomainNameParser
38
+ attr_reader :domain_basename, :domain_name, :feature, :filters, :order_by
39
+
40
+
41
+ #
42
+ # 1) Inputs form the SearchTransform.apply
43
+ # search:
44
+ # domain: [String],
45
+ # filters: [Expr|ExprConjunction]
46
+ # orders: [OrderExpr|Array[OrderExpr]]
47
+ # limit: [Integer]
48
+ #
49
+ # 2) Inputs manually built from a developer
50
+ # search:
51
+ # domain: [String],
52
+ # filters: [String|Array[String|Hash]]
53
+ # orders: [String|Array[String|Hash]]
54
+ # limit: [Integer]
55
+ #
56
+ #
57
+ # 3) Inputs as a full search string
58
+ # search: [String]
59
+ #
60
+ # domain: String,
61
+ # filters: String | Expr | ExprConjunction
62
+ # order: String | Array[OrderExpr|String|Hash]
63
+ # limit: Integer
64
+ #
65
+ #
66
+ def self.build(inputs)
67
+ unless inputs.key?(:domain)
68
+ fail "search criteria :domain is required"
69
+ end
70
+ criteria = self.new(inputs[:domain])
71
+ criteria.filter(inputs[:filters])
72
+
73
+ if inputs.key?(:order)
74
+ criteria.order(inputs[:order])
75
+ end
76
+
77
+ if inputs.key?(:limit)
78
+ criteria.limit(inputs[:limit])
79
+ end
80
+ criteria
81
+ end
82
+
83
+ # Parse out the domain into feature, domain, determine the name of the
84
+ # repo this criteria is for and initailize basic settings.
85
+ # global.user
86
+ #
87
+ # membership.user
88
+ # foo.id filter name like "foo" order foo.bar.id asc limit 2
89
+ # foo.id exists foo.id = 5
90
+ #
91
+ # @example
92
+ # SpCore::Domain::Criteria('foo', single: true)
93
+ # Types.Criteria('foo.bar', single: true)
94
+ #
95
+ # === Options
96
+ # error_on_empty: will cause the repo to fail when query returns an
97
+ # an empty dataset. The failure will have the message
98
+ # with key as domain and text is "<domain> not found"
99
+ #
100
+ # single: will cause the repo to return only one, the first,
101
+ # entity in the dataset
102
+ #
103
+ # @param domain [String] fully qualified domain name
104
+ # @param opts [Hash] options for initializing criteria
105
+ # @return [Criteria]
106
+ def initialize(domain_name, data = {})
107
+ @feature, @domain_basename, @domain_name = parse_domain_name(domain_name)
108
+ @filters = nil
109
+ @params = {}
110
+ @parser = data[:expr_parser] || ExprParser.new
111
+ @transform = data[:expr_transform] || ExprTransform.new
112
+ @limit = nil
113
+ @order_by = []
114
+ filter(data[:filters]) if data[:filters]
115
+ end
116
+
117
+ def clear_filters
118
+ @filters = nil
119
+ end
120
+
121
+ def filters?
122
+ !filters.nil?
123
+ end
124
+
125
+ def global?
126
+ !feature?
127
+ end
128
+
129
+ def feature?
130
+ @feature != 'global'
131
+ end
132
+
133
+ # @example
134
+ # criteria.add_param('foo', 100)
135
+ #
136
+ # @param key [Symbol, String] The key name where we want to keep the value
137
+ # @param value [String, Integer] The value that belongs to the key param
138
+ # @return [String, Integer] The saved value
139
+ def add_param(key, value)
140
+ fail 'key should not be nil' if key.nil?
141
+
142
+ @params[key.to_sym] = value
143
+ end
144
+
145
+ # @param key [String, Symbol]
146
+ # @return [String, Integer, Boolean] the found value
147
+ def param(key)
148
+ @params[key.to_sym]
149
+ end
150
+
151
+ # @param key [String, Symbol]
152
+ # @return [Boolean]
153
+ def param?(key)
154
+ @params.key?(key.to_sym)
155
+ end
156
+
157
+ # @return [Boolean] if the @params variable has values
158
+ def params?
159
+ !@params.empty?
160
+ end
161
+
162
+ # [
163
+ # 'id = 6',
164
+ # {'name like "foo"' => 'or'},
165
+ # ]
166
+ #
167
+ #
168
+ def filter_array(input)
169
+ unless input.respond_to?(:each)
170
+ fail "input must implement :each, expecting a list"
171
+ end
172
+
173
+ input.each do |item|
174
+ filter(item)
175
+ end
176
+ end
177
+
178
+ def filter_string(expr, op: 'and')
179
+ expr = parse_expr(expr)
180
+ fail "Could not parse (#{expr}) unkown failure" unless expr
181
+ filter_expr(expr, op: op)
182
+ self
183
+ end
184
+
185
+ #
186
+ # filters: {
187
+ # 'id = 6' => :and,
188
+ # 'id = 8' => :and,
189
+ # }
190
+ def filter_hash(input)
191
+ unless input.respond_to?(:each)
192
+ fail "input must implement :each, expecting a hash"
193
+ end
194
+
195
+ input.each do |expr, op|
196
+ filter_string(expr, op: op)
197
+ end
198
+
199
+ self
200
+ end
201
+
202
+ def filter_expr(expr, op: 'and')
203
+ expr = qualify_expr(expr)
204
+ if filters?
205
+ expr = expr_conjunction_class.new(op, filters, expr)
206
+ end
207
+ @filters = expr
208
+ self
209
+ end
210
+
211
+ def filter(item, op: 'and')
212
+ case
213
+ when item.is_a?(Array) then filter_array(item)
214
+ when item.is_a?(Hash) then filter_hash(item)
215
+ when item.is_a?(String) then filter_string(item, op: op)
216
+ when item.instance_of?(expr_class),
217
+ item.instance_of?(expr_conjunction_class)
218
+ filter_expr(item, op: op)
219
+ else
220
+ fail "filter could not understand input (#{input})"
221
+ end
222
+ self
223
+ end
224
+
225
+ def limit?
226
+ !@limit.nil?
227
+ end
228
+
229
+ def limit(nbr = nil)
230
+ return @limit if nbr.nil?
231
+
232
+ @limit = Integer(nbr)
233
+ fail "limit must be an integer greater than 0" unless nbr > 0
234
+ self
235
+ end
236
+
237
+ def order?
238
+ !@order_by.empty?
239
+ end
240
+
241
+ # order first_name asc, last_name
242
+ # order: 'foo.bar.id asc'
243
+ # order: 'foo.bar.id'
244
+ # order: [
245
+ # 'foo.bar.id',
246
+ # 'foo.bar.id asc',
247
+ # {'foo.bar.id => 'desc'},
248
+ # {'foo.bar.code => 'asc'},
249
+ # ]
250
+ #
251
+ # membership.user.id
252
+ def order(data)
253
+ order_exprs(OrderExpr.build(data))
254
+ end
255
+
256
+ def order_expr(expr)
257
+ @order_by << qualify_expr(expr)
258
+ self
259
+ end
260
+
261
+ def order_exprs(list)
262
+ list.each do |expr|
263
+ order_expr(expr)
264
+ end
265
+ self
266
+ end
267
+
268
+ private
269
+ attr_reader :parser, :transform
270
+
271
+ def expr_class
272
+ Expr
273
+ end
274
+
275
+ def expr_conjunction_class
276
+ ExprConjunction
277
+ end
278
+
279
+ def parse_expr(str)
280
+ if !(parser && parser.respond_to?(:parse))
281
+ fail "expression parser must implement :parse"
282
+ end
283
+
284
+ if !(transform && transform.respond_to?(:apply))
285
+ fail "expression transform must implement :apply"
286
+ end
287
+
288
+ begin
289
+ tree = parser.parse(str)
290
+ rescue Parslet::ParseFailed => e
291
+ msg = "The expression (#{str}) failed to parse"
292
+ err = RuntimeError.new(msg)
293
+ err.set_backtrace(e.backtrace)
294
+ raise err
295
+ end
296
+
297
+ result = transform.apply(tree)
298
+ result = result[:domain_expr] || result[:root]
299
+ unless result
300
+ fail "unable to parse (#{str}) correctly"
301
+ end
302
+ result
303
+ end
304
+
305
+ def qualify_expr(domain_expr)
306
+ return domain_expr if domain_expr.qualified?
307
+ if global?
308
+ domain_expr.qualify_global(domain_basename)
309
+ return domain_expr
310
+ end
311
+
312
+ domain_expr.qualify_feature(feature, domain_basename)
313
+ domain_expr
314
+ end
315
+ end
316
+ end
317
+ end
@@ -1,5 +1,5 @@
1
1
  module Appfuel
2
- module Domain
2
+ module Repository
3
3
  # Domain expressions are used mostly by the criteria to describe filter
4
4
  # conditions. The class represents a basic expression like "id = 6", the
5
5
  # problem with this expression is that "id" is relative to the domain
@@ -0,0 +1,41 @@
1
+ module Appfuel
2
+ module Repository
3
+ class ExprConjunction
4
+ OPERATORS = ['and', 'or'].freeze
5
+ attr_reader :op, :left, :right
6
+
7
+ def initialize(type, left, right)
8
+ @op = validate_operator(type)
9
+ @left = left
10
+ @right = right
11
+ end
12
+
13
+ def conjunction?
14
+ true
15
+ end
16
+
17
+ def qualified?
18
+ left.qualified? && right.qualified?
19
+ end
20
+
21
+ def qualify_feature(feature, domain)
22
+ left.qualify_feature(feature, domain) unless left.qualified?
23
+ right.qualify_feature(feature, domain) unless right.qualified?
24
+ end
25
+
26
+ def qualify_global(domain)
27
+ left.qualify_global(domain) unless left.qualified?
28
+ right.qualify_global(domain) unless right.qualified?
29
+ end
30
+
31
+ private
32
+ def validate_operator(type)
33
+ type = type.to_s.downcase
34
+ unless OPERATORS.include?(type)
35
+ fail "Conjunction operator can only be (and|or)"
36
+ end
37
+ type
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,16 +1,11 @@
1
- require 'parslet'
2
- require 'parslet/convenience'
3
- require_relative 'expr_transform'
4
-
5
1
  module Appfuel
6
- module Domain
2
+ module Repository
7
3
  # A PEG (Parser Expression Grammer) parser for our domain language. This
8
4
  # gives us the ablity to describe the filtering we would like to do when
9
5
  # searching on a given domain entity. The search string is parsed and
10
6
  # transformed into an interface that repositories can use to determine how
11
7
  # to search a given storage interface. The language always represent the
12
8
  # business domain entities and not a storage system.
13
- #
14
9
  class ExprParser < Parslet::Parser
15
10
  rule(:space) { match('\s').repeat(1) }
16
11
  rule(:space?) { space.maybe }
@@ -21,10 +16,6 @@ module Appfuel
21
16
  rule(:lparen) { str('(') >> space? }
22
17
  rule(:rparen) { str(')') >> space? }
23
18
 
24
- rule(:filter_identifier) { stri('filter') }
25
- rule(:order_identifier) { stri('order') }
26
- rule(:limit_identifier) { stri('limit') }
27
-
28
19
  rule(:integer) do
29
20
  (str('-').maybe >> digit >> digit.repeat).as(:integer)
30
21
  end
@@ -83,15 +74,13 @@ module Appfuel
83
74
  (attr_label >> (str('.') >> attr_label).repeat).maybe.as(:domain_attr)
84
75
  end
85
76
 
86
-
87
-
88
77
  rule(:and_op) { stri('and') >> space? }
89
78
  rule(:or_op) { stri('or') >> space? }
90
- rule(:in_op) { stri('in') >> space? }
91
- rule(:like_op) { stri('like') >> space? }
92
- rule(:between_op) { stri('between') >> space? }
79
+ rule(:in_op) { (stri('in') | stri('not in')) >> space? }
80
+ rule(:like_op) { (stri('like') | stri('not like')) >> space? }
81
+ rule(:between_op) { (stri('between') | stri('not between')) >> space? }
93
82
 
94
- rule(:eq_op) { str('=') >> space? }
83
+ rule(:eq_op) { (str('=') | str('!=')) >> space? }
95
84
  rule(:gt_op) { str('>') >> space? }
96
85
  rule(:gteq_op) { str('>=') >> space? }
97
86
  rule(:lt_op) { str('<') >> space? }
@@ -170,22 +159,21 @@ module Appfuel
170
159
  lparen >> or_operation >> rparen | domain_expr >> space?
171
160
  end
172
161
 
162
+
173
163
  rule(:and_operation) do
174
164
  (
175
- primary.as(:left) >> space? >>
176
- and_op >>
177
- and_operation.as(:right)
165
+ primary.as(:left) >> and_op >> and_operation.as(:right) >> space?
178
166
  ).as(:and) | primary
179
167
  end
180
168
 
181
169
  rule(:or_operation) do
182
170
  (
183
- and_operation.as(:left) >> space? >>
184
- or_op >>
185
- or_operation.as(:right)
171
+ and_operation.as(:left) >> or_op >> or_operation.as(:right)
186
172
  ).as(:or) | and_operation
187
173
  end
188
174
 
175
+ # rule for domain
176
+ #
189
177
  root(:or_operation)
190
178
 
191
179
  def stri(str)