appfuel 0.2.5 → 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/appfuel.gemspec +1 -1
- data/lib/appfuel/application/app_container.rb +0 -1
- data/lib/appfuel/application/dispatcher.rb +32 -0
- data/lib/appfuel/application/root.rb +4 -4
- data/lib/appfuel/application.rb +2 -1
- data/lib/appfuel/domain/domain_name_parser.rb +9 -5
- data/lib/appfuel/domain.rb +0 -7
- data/lib/appfuel/handler/base.rb +8 -14
- data/lib/appfuel/storage/db/mapper.rb +31 -35
- data/lib/appfuel/storage/db/repository.rb +53 -121
- data/lib/appfuel/storage/repository/base.rb +246 -0
- data/lib/appfuel/storage/repository/criteria.rb +317 -0
- data/lib/appfuel/{domain → storage/repository}/expr.rb +1 -1
- data/lib/appfuel/storage/repository/expr_conjunction.rb +41 -0
- data/lib/appfuel/{domain → storage/repository}/expr_parser.rb +10 -22
- data/lib/appfuel/{domain → storage/repository}/expr_transform.rb +7 -7
- data/lib/appfuel/{repository → storage/repository}/mapper.rb +8 -3
- data/lib/appfuel/storage/repository/order_expr.rb +51 -0
- data/lib/appfuel/storage/repository/runner.rb +62 -0
- data/lib/appfuel/storage/repository/search_parser.rb +50 -0
- data/lib/appfuel/storage/repository/search_transform.rb +58 -0
- data/lib/appfuel/{domain/criteria_settings.rb → storage/repository/settings.rb} +20 -5
- data/lib/appfuel/{repository.rb → storage/repository.rb} +13 -0
- data/lib/appfuel/storage.rb +1 -0
- data/lib/appfuel/version.rb +1 -1
- data/lib/appfuel.rb +0 -2
- metadata +21 -19
- data/lib/appfuel/domain/base_criteria.rb +0 -171
- data/lib/appfuel/domain/exists_criteria.rb +0 -57
- data/lib/appfuel/domain/expr_conjunction.rb +0 -27
- data/lib/appfuel/domain/search_criteria.rb +0 -137
- data/lib/appfuel/repository/base.rb +0 -86
- data/lib/appfuel/repository_runner.rb +0 -60
- /data/lib/appfuel/{repository → storage/repository}/initializer.rb +0 -0
- /data/lib/appfuel/{repository → storage/repository}/mapping_dsl.rb +0 -0
- /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
|
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
|
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('=')
|
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) >>
|
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)
|