appfuel 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +78 -1
  4. data/appfuel.gemspec +1 -0
  5. data/docs/images/appfuel_basic_flow.png +0 -0
  6. data/lib/appfuel/application/app_container.rb +24 -1
  7. data/lib/appfuel/application/container_class_registration.rb +29 -4
  8. data/lib/appfuel/application/root.rb +1 -0
  9. data/lib/appfuel/application.rb +0 -1
  10. data/lib/appfuel/domain/base_criteria.rb +171 -0
  11. data/lib/appfuel/domain/criteria_builder.rb +248 -0
  12. data/lib/appfuel/domain/criteria_settings.rb +156 -0
  13. data/lib/appfuel/domain/dsl.rb +5 -1
  14. data/lib/appfuel/domain/entity.rb +1 -2
  15. data/lib/appfuel/domain/exists_criteria.rb +57 -0
  16. data/lib/appfuel/domain/expr.rb +66 -97
  17. data/lib/appfuel/domain/expr_conjunction.rb +27 -0
  18. data/lib/appfuel/domain/expr_parser.rb +199 -0
  19. data/lib/appfuel/domain/expr_transform.rb +68 -0
  20. data/lib/appfuel/domain/search_criteria.rb +137 -0
  21. data/lib/appfuel/domain.rb +6 -1
  22. data/lib/appfuel/feature/initializer.rb +5 -0
  23. data/lib/appfuel/handler/action.rb +3 -0
  24. data/lib/appfuel/handler/base.rb +11 -1
  25. data/lib/appfuel/handler/command.rb +4 -0
  26. data/lib/appfuel/repository/base.rb +16 -2
  27. data/lib/appfuel/repository/mapper.rb +41 -7
  28. data/lib/appfuel/repository/mapping_dsl.rb +4 -4
  29. data/lib/appfuel/repository/mapping_entry.rb +2 -2
  30. data/lib/appfuel/repository.rb +0 -1
  31. data/lib/appfuel/storage/db/active_record_model.rb +32 -28
  32. data/lib/appfuel/storage/db/mapper.rb +38 -125
  33. data/lib/appfuel/storage/db/repository.rb +6 -10
  34. data/lib/appfuel/storage/memory/repository.rb +4 -0
  35. data/lib/appfuel/types.rb +0 -1
  36. data/lib/appfuel/version.rb +1 -1
  37. data/lib/appfuel.rb +6 -10
  38. metadata +26 -7
  39. data/lib/appfuel/application/container_key.rb +0 -201
  40. data/lib/appfuel/application/qualify_container_key.rb +0 -76
  41. data/lib/appfuel/db_model.rb +0 -16
  42. data/lib/appfuel/domain/criteria.rb +0 -436
  43. data/lib/appfuel/repository/mapping_registry.rb +0 -121
@@ -0,0 +1,156 @@
1
+ module Appfuel
2
+ module Domain
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
+ # settings:
11
+ # page: 1
12
+ # per_page: 2
13
+ # disable_pagination
14
+ # first
15
+ # last
16
+ # all
17
+ # error_on_empty
18
+ # parser
19
+ # transform
20
+ # search_name
21
+ #
22
+ class CriteriaSettings
23
+ DEFAULT_PAGE = 1
24
+ DEFAULT_PER_PAGE = 20
25
+
26
+ attr_reader :parser, :transform
27
+
28
+ # @param domain [String] fully qualified domain name
29
+ # @param opts [Hash] options for initializing criteria
30
+ # @return [Criteria]
31
+ def initialize(settings = {})
32
+ @parser = settings[:expr_parser] || ExprParser.new
33
+ @transform = settings[:expr_transform] || ExprTransform.new
34
+
35
+
36
+ empty_dataset_is_valid!
37
+ enable_pagination
38
+ disable_all
39
+ disable_first
40
+ disable_last
41
+
42
+ if settings[:error_on_empty] == true
43
+ error_on_empty_dataset!
44
+ end
45
+
46
+ if settings[:disable_pagination] == true
47
+ disable_pagination
48
+ end
49
+
50
+ if settings[:first] == true
51
+ first
52
+ elsif settings[:last] == true
53
+ last
54
+ elsif settings[:all]
55
+ all
56
+ end
57
+
58
+ page(settings[:page] || DEFAULT_PAGE)
59
+ per_page(settings[:per_page] || DEFAULT_PER_PAGE)
60
+ end
61
+
62
+ def disable_pagination?
63
+ @disable_pagination
64
+ end
65
+
66
+ def enable_pagination
67
+ @disable_pagination = false
68
+ self
69
+ end
70
+
71
+ def disable_pagination
72
+ @disable_pagination = true
73
+ self
74
+ end
75
+
76
+ def page(value = nil)
77
+ return @page if value.nil?
78
+ @page = Integer(value)
79
+ self
80
+ end
81
+
82
+ def per_page(value = nil)
83
+ return @per_page if value.nil?
84
+ @per_page = Integer(value)
85
+ self
86
+ end
87
+
88
+ def all?
89
+ @all
90
+ end
91
+
92
+ def all
93
+ @all = true
94
+ disable_first
95
+ disable_last
96
+ self
97
+ end
98
+
99
+ def first?
100
+ @first
101
+ end
102
+
103
+ def first
104
+ @first = true
105
+ disable_last
106
+ disable_all
107
+ self
108
+ end
109
+
110
+ def last?
111
+ @last
112
+ end
113
+
114
+ def last
115
+ @last = true
116
+ disable_first
117
+ disable_all
118
+ self
119
+ end
120
+
121
+ # Tells the repo to return an error when entity is not found
122
+ #
123
+ # @return SearchSettings
124
+ def error_on_empty_dataset!
125
+ @error_on_empty = true
126
+ self
127
+ end
128
+
129
+ # Tells the repo to return and empty collection, or nil if single is
130
+ # invoked, if the entity is not found
131
+ #
132
+ # @return SearchSettings
133
+ def empty_dataset_is_valid!
134
+ @error_on_empty = false
135
+ self
136
+ end
137
+
138
+ def error_on_empty_dataset?
139
+ @error_on_empty
140
+ end
141
+
142
+ private
143
+ def disable_all
144
+ @all = false
145
+ end
146
+
147
+ def disable_first
148
+ @first = false
149
+ end
150
+
151
+ def disable_last
152
+ @last = false
153
+ end
154
+ end
155
+ end
156
+ end
@@ -27,10 +27,14 @@ module Appfuel
27
27
  klass.equalizer = Dry::Equalizer.new(*schema.keys)
28
28
  klass.send(:include, klass.equalizer)
29
29
 
30
- register_container_class(klass)
30
+ stage_class_for_registration(klass)
31
31
  Types.register_domain(klass)
32
32
  end
33
33
 
34
+ def container_class_type
35
+ 'domains'
36
+ end
37
+
34
38
  def default?
35
39
  false
36
40
  end
@@ -10,8 +10,7 @@ module Appfuel
10
10
  #
11
11
  module Domain
12
12
  class Entity
13
- extend Appfuel::Application::ContainerKey
14
- extend Appfuel::Application::ContainerClassRegistration
13
+ include Appfuel::Application::AppContainer
15
14
  extend Dsl
16
15
 
17
16
  def initialize(inputs = {})
@@ -0,0 +1,57 @@
1
+ module Appfuel
2
+ module Domain
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
+ #
15
+ # exits:
16
+ # domain: 'foo.bar'
17
+ # expr: 'id = 6'
18
+ #
19
+ # settings:
20
+ # error_on_empty
21
+ # parser
22
+ # transform
23
+ #
24
+ #
25
+ class ExistsCriteria < BaseCriteria
26
+ #
27
+ # @param domain [String] fully qualified domain name
28
+ # @param opts [Hash] options for initializing criteria
29
+ # @return [Criteria]
30
+ def initialize(domain_name, data = {})
31
+ super
32
+ expr(data[:expr]) if data[:expr]
33
+ end
34
+
35
+ def filter(str)
36
+ domain_expr = parse_expr(str)
37
+ if filters?
38
+ fail "A filter expression has already been assigned"
39
+ end
40
+
41
+ if domain_expr.conjunction?
42
+ fail "Only simple domain expressions are allowed for exists criteria"
43
+ end
44
+
45
+ if domain_expr.qualified?
46
+ fail "Only allows relative domain attributes"
47
+ end
48
+
49
+ @filters = qualify_expr(domain_expr)
50
+ self
51
+ end
52
+
53
+ private
54
+
55
+ end
56
+ end
57
+ end
@@ -1,126 +1,95 @@
1
1
  module Appfuel
2
2
  module Domain
3
3
  # Domain expressions are used mostly by the criteria to describe filter
4
- # conditions. The class represents a basic expression like "id eq 6", the
5
- # problem with this expression is that we need additional information in
6
- # order to properly map it to something like a db expression. This call
7
- # ensures that additional information exists. Most importantly we need
8
- # a fully qualified domain name in the form of "feature.domain".
4
+ # conditions. The class represents a basic expression like "id = 6", the
5
+ # problem with this expression is that "id" is relative to the domain
6
+ # represented by the criteria. In order to convert that expression to a
7
+ # storage expression for a db, that expression must be fully qualified in
8
+ # the form of "features.feature_name.domain.id = 6" so that the mapper can
9
+ # correctly map to database attributes. This class provides the necessary
10
+ # interfaces to allow a criteria to qualify all of its relative expressions.
11
+ # It also allows fully qualifed expressions to be used.
9
12
  class Expr
10
- include DomainNameParser
11
- OPS = {
12
- eq: '=',
13
- gt: '>',
14
- gteq: '>=',
15
- lt: '<',
16
- lteq: '<=',
17
- in: 'IN',
18
- like: 'LIKE',
19
- ilike: 'ILIKE',
20
- between: 'BETWEEN'
21
- }
22
- attr_reader :feature, :domain_basename, :domain_name, :domain_attr, :value
23
-
24
- # Assign the fully qualified domain name, its basename and its attribute
25
- # along with the operator and value. Operator and value are assumed to
26
- # be the first key => value pair of the hash.
27
- #
28
- # @example
29
- # feature domain
30
- # Expr.new('foo.bar', 'id', eq: 6)
31
- #
32
- # or
33
- # global domain
34
- # Expr.new('bar', 'name', like: '%Bob%')
35
- #
36
- #
37
- # @param domain [String] fully qualified domain name
38
- # @param domain_attr [String, Symbol] attribute name
39
- # @param data [Hash] holds operator and value
40
- # @option data [Symbol] the key is the operator and value is the value
41
- #
42
- # @return [Expr]
43
- def initialize(domain, domain_attr, data)
44
- fail "operator value pair must exist in a hash" unless data.is_a?(Hash)
45
- @feature, @domain_basename, @domain_name = parse_domain_name(domain)
46
-
47
- operator, value = data.first
48
- @domain_attr = domain_attr.to_s
49
- self.op = operator
50
- self.value = value
51
-
52
- fail "domain name can not be empty" if @domain_name.empty?
53
- fail "domain attribute can not be empty" if @domain_attr.empty?
13
+ attr_reader :feature, :domain_basename, :domain_attr, :attr_list, :op, :value
14
+
15
+ def initialize(domain_attr, op, value)
16
+ @attr_list = parse_domain_attr(domain_attr)
17
+ @op = op.to_s.strip
18
+ @value = value
19
+
20
+ fail "op can not be empty" if @op.empty?
21
+ fail "attr_list can not be empty" if @attr_list.empty?
22
+ end
23
+
24
+ def qualify_feature(feature, domain)
25
+ fail "this expr is already qualified" if qualified?
26
+
27
+ attr_list.unshift(domain)
28
+ attr_list.unshift(feature)
29
+ attr_list.unshift('features')
30
+ self
54
31
  end
55
32
 
56
- def feature?
57
- !@feature.nil?
33
+ def qualify_global(domain)
34
+ fail "this expr is already qualified" if qualified?
35
+ attr_list.unshift(domain)
36
+ attr_list.unshift('global')
37
+ self
58
38
  end
59
39
 
60
40
  def global?
61
- !feature?
41
+ attr_list[0] == 'global'
62
42
  end
63
43
 
64
- # @return [Bool]
65
- def negated?
66
- @negated
44
+ def conjunction?
45
+ false
67
46
  end
68
47
 
69
- def expr_string
70
- data = yield domain_name, domain_attr, OPS[op]
71
- lvalue = data[0]
72
- operator = data[1]
73
- rvalue = data[2]
48
+ def qualified?
49
+ attr_list[0] == 'global' || attr_list[0] == 'features'
50
+ end
74
51
 
75
- operator = "NOT #{operator}" if negated?
76
- "#{lvalue} #{operator} #{rvalue}"
52
+ def feature
53
+ index = global? ? 0 : 1
54
+ attr_list[index]
77
55
  end
78
56
 
79
- def to_s
80
- "#{domain_name}.#{domain_attr} #{OPS[op]} #{value}"
57
+ def domain_basename
58
+ index = global? ? 1 : 2
59
+ attr_list[index]
81
60
  end
82
61
 
83
- def op
84
- negated? ? "not_#{@op}".to_sym : @op
62
+ def domain_name
63
+ "#{feature}.#{domain_basename}"
85
64
  end
86
65
 
87
- private
66
+ def domain_attr
67
+ start_range = global? ? 2 : 3
68
+ end_range = -1
69
+ attr_list.slice(start_range .. end_range).join('.')
70
+ end
88
71
 
89
- def op=(value)
90
- negated, value = value.to_s.split('_')
91
- @negated = false
92
- if negated == 'not'
93
- @negated = true
94
- else
95
- value = negated
96
- end
97
- value = value.to_sym
98
- unless supported_op?(value)
99
- fail "op has to be one of [#{OPS.keys.join(',')}]"
100
- end
101
- @op = value
72
+ def to_s
73
+ "#{attr_list.join('.')} #{op} #{value}"
102
74
  end
103
75
 
104
- def value=(data)
105
- case op
106
- when :in
107
- unless data.is_a?(Array)
108
- fail ":in operator must have an array as a value"
109
- end
110
- when :range
111
- unless data.is_a?(Range)
112
- fail ":range operator must have a range as a value"
113
- end
114
- when :gt, :gteq, :lt, :lteq
115
- unless data.is_a?(Numeric)
116
- fail ":gt, :gteq, :lt, :lteq operators expect a numeric value"
117
- end
76
+ def validate_as_fully_qualified
77
+ unless qualified?
78
+ fail "expr (#{to_s}) is not fully qualified, mapping will not work"
118
79
  end
119
- @value = data
80
+ true
120
81
  end
121
82
 
122
- def supported_op?(op)
123
- OPS.keys.include?(op)
83
+ private
84
+
85
+ def parse_domain_attr(list)
86
+ list = list.split('.') if list.is_a?(String)
87
+
88
+ unless list.is_a?(Array)
89
+ fail "Domain attribute must be a string in the form of " +
90
+ "(foo.bar.id) or an array ['foo', 'bar', 'id']"
91
+ end
92
+ list
124
93
  end
125
94
  end
126
95
  end
@@ -0,0 +1,27 @@
1
+ module Appfuel
2
+ module Domain
3
+ class ExprConjunction
4
+ attr_reader :op, :left, :right
5
+
6
+ def initialize(type, left, right)
7
+ @op = type.to_s.downcase
8
+ @left = left
9
+ @right = right
10
+ end
11
+
12
+ def conjunction?
13
+ true
14
+ end
15
+
16
+ def qualify_feature(feature, domain)
17
+ left.qualify_feature(feature, domain)
18
+ right.qualify_feature(feature, domain)
19
+ end
20
+
21
+ def qualify_global(domain)
22
+ left.qualify_global(domain)
23
+ right.qualify_global(domain)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,199 @@
1
+ require 'parslet'
2
+ require 'parslet/convenience'
3
+ require_relative 'expr_transform'
4
+
5
+ module Appfuel
6
+ module Domain
7
+ # A PEG (Parser Expression Grammer) parser for our domain language. This
8
+ # gives us the ablity to describe the filtering we would like to do when
9
+ # searching on a given domain entity. The search string is parsed and
10
+ # transformed into an interface that repositories can use to determine how
11
+ # to search a given storage interface. The language always represent the
12
+ # business domain entities and not a storage system.
13
+ #
14
+ class ExprParser < Parslet::Parser
15
+ rule(:space) { match('\s').repeat(1) }
16
+ rule(:space?) { space.maybe }
17
+
18
+ rule(:comma) { space? >> str(',') >> space? }
19
+ rule(:digit) { match['0-9'] }
20
+
21
+ rule(:lparen) { str('(') >> space? }
22
+ rule(:rparen) { str(')') >> space? }
23
+
24
+ rule(:filter_identifier) { stri('filter') }
25
+ rule(:order_identifier) { stri('order') }
26
+ rule(:limit_identifier) { stri('limit') }
27
+
28
+ rule(:integer) do
29
+ (str('-').maybe >> digit >> digit.repeat).as(:integer)
30
+ end
31
+
32
+ rule(:float) do
33
+ (
34
+ str('-').maybe >> digit.repeat(1) >> str('.') >> digit.repeat(1)
35
+ ).as(:float)
36
+ end
37
+
38
+ rule(:number) { integer | float }
39
+
40
+ rule(:boolean) do
41
+ (stri("true") | stri('false')).as(:boolean)
42
+ end
43
+
44
+ rule(:string_special) { match['\0\t\n\r"\\\\'] }
45
+
46
+ rule(:escaped_special) { str('\\') >> match['0tnr"\\\\'] }
47
+
48
+ rule(:string) do
49
+ str('"') >>
50
+ ((escaped_special | string_special.absent? >> any).repeat).as(:string) >>
51
+ str('"')
52
+ end
53
+
54
+ rule(:date) do
55
+ (
56
+ digit.repeat(4) >> str('-') >>
57
+ digit.repeat(2) >> str('-') >>
58
+ digit.repeat(2)
59
+ ).as(:date)
60
+ end
61
+
62
+ # 1979-05-27T07:32:00Z
63
+ rule(:datetime) do
64
+ (
65
+ digit.repeat(4) >> str('-') >>
66
+ digit.repeat(2) >> str('-') >>
67
+ digit.repeat(2) >> str("T") >>
68
+ digit.repeat(2) >> str(":") >>
69
+ digit.repeat(2) >> str(":") >>
70
+ digit.repeat(2) >> str("Z")
71
+ ).as(:datetime)
72
+ end
73
+
74
+ rule(:value) do
75
+ string | number | boolean | datetime | date
76
+ end
77
+
78
+ rule(:attr_label) do
79
+ match['a-z0-9_'].repeat(1).as(:attr_label)
80
+ end
81
+
82
+ rule(:domain_attr) do
83
+ (attr_label >> (str('.') >> attr_label).repeat).maybe.as(:domain_attr)
84
+ end
85
+
86
+
87
+
88
+ rule(:and_op) { stri('and') >> space? }
89
+ 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? }
93
+
94
+ rule(:eq_op) { str('=') >> space? }
95
+ rule(:gt_op) { str('>') >> space? }
96
+ rule(:gteq_op) { str('>=') >> space? }
97
+ rule(:lt_op) { str('<') >> space? }
98
+ rule(:lteq_op) { str('<=') >> space? }
99
+
100
+ rule(:comparison_value) do
101
+ number | date | datetime
102
+ end
103
+
104
+ rule(:eq_expr) do
105
+ domain_attr >> space? >> eq_op.as(:op) >> value.as(:value)
106
+ end
107
+
108
+ rule(:gt_expr) do
109
+ domain_attr >> space? >>
110
+ gt_op.as(:op) >> space? >>
111
+ comparison_value.as(:value)
112
+ end
113
+
114
+ rule(:gteq_expr) do
115
+ domain_attr >> space? >>
116
+ gteq_op.as(:op) >> space? >>
117
+ comparison_value.as(:value)
118
+ end
119
+
120
+ rule(:lt_expr) do
121
+ domain_attr >> space? >>
122
+ lt_op.as(:op) >> space? >>
123
+ comparison_value.as(:value)
124
+ end
125
+
126
+ rule(:lteq_expr) do
127
+ domain_attr >> space? >>
128
+ lteq_op.as(:op) >> space? >>
129
+ comparison_value.as(:value)
130
+ end
131
+
132
+ rule(:relational_expr) do
133
+ eq_expr | gt_expr | gteq_expr | lt_expr | lteq_expr
134
+ end
135
+
136
+ rule(:in_expr) do
137
+ domain_attr >> space >>
138
+ in_op.as(:op) >>
139
+ str('(') >> space? >>
140
+ (value >> (comma >> value).repeat).maybe.as(:value) >> space? >>
141
+ str(')')
142
+ end
143
+
144
+ rule(:like_expr) do
145
+ domain_attr >> space >>
146
+ like_op.as(:op) >> space? >>
147
+ string.as(:value)
148
+ end
149
+
150
+ rule(:between_expr) do
151
+ domain_attr >> space? >>
152
+ between_op.as(:op) >> space? >>
153
+ (
154
+ comparison_value.as(:lvalue) >> space? >>
155
+ and_op >> space? >>
156
+ comparison_value.as(:rvalue)
157
+ ).as(:value)
158
+ end
159
+
160
+ rule(:domain_expr) do
161
+ (
162
+ relational_expr |
163
+ like_expr |
164
+ between_expr |
165
+ in_expr
166
+ ).as(:domain_expr)
167
+ end
168
+
169
+ rule(:primary) do
170
+ lparen >> or_operation >> rparen | domain_expr >> space?
171
+ end
172
+
173
+ rule(:and_operation) do
174
+ (
175
+ primary.as(:left) >> space? >>
176
+ and_op >>
177
+ and_operation.as(:right)
178
+ ).as(:and) | primary
179
+ end
180
+
181
+ rule(:or_operation) do
182
+ (
183
+ and_operation.as(:left) >> space? >>
184
+ or_op >>
185
+ or_operation.as(:right)
186
+ ).as(:or) | and_operation
187
+ end
188
+
189
+ root(:or_operation)
190
+
191
+ def stri(str)
192
+ key_chars = str.split(//)
193
+ key_chars.collect! {|char|
194
+ match["#{char.upcase}#{char.downcase}"]
195
+ }.reduce(:>>)
196
+ end
197
+ end
198
+ end
199
+ end