appfuel 0.2.3 → 0.2.4

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 (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