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