appfuel 0.2.5 → 0.2.6

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.
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)