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,68 @@
1
+ require 'parslet'
2
+
3
+ module Appfuel
4
+ module Domain
5
+ # A PEG (Parser Expression Grammer) transformer for our domain language.
6
+ #
7
+ class ExprTransform < Parslet::Transform
8
+ rule(integer: simple(:n)) { Integer(n) }
9
+ rule(float: simple(:n)) { Float(n) }
10
+ rule(boolean: simple(:b)) { b.downcase == 'true' }
11
+ rule(datetime: simple(:dt)) { Time.parse(dt) }
12
+ rule(date: simple(:d)) { Date.parse(d) }
13
+ rule(string: simple(:s)) do
14
+ s.to_s.gsub(/\\[0tnr]/, "\\0" => "\0",
15
+ "\\t" => "\t",
16
+ "\\n" => "\n",
17
+ "\\r" => "\r")
18
+ end
19
+
20
+ rule(domain_expr: subtree(:expr)) do |dict|
21
+ expr = dict[:expr]
22
+ attrs = build_domain_attrs(expr[:domain_attr])
23
+ op = expr[:op].to_s.strip.downcase
24
+ value = expr[:value]
25
+ {domain_expr: Expr.new(attrs, op, value)}
26
+ end
27
+
28
+ rule(and: subtree(:data)) do |dict|
29
+ {root: build_conjunction('and', dict[:data])}
30
+ end
31
+
32
+ rule(or: subtree(:data)) do |dict|
33
+ {root: build_conjunction('or', dict[:data])}
34
+ end
35
+
36
+ def self.build_conjunction(op, data)
37
+ left = build_conjunction_node(data[:left])
38
+ right = build_conjunction_node(data[:right])
39
+
40
+ ExprConjunction.new(op, left, right)
41
+ end
42
+
43
+ def self.build_conjunction_node(data)
44
+ if data.key?(:root)
45
+ node = data[:root]
46
+ elsif data.key?(:domain_expr)
47
+ node = data[:domain_expr]
48
+ elsif data.key?(:and) || data.key?(:or)
49
+ op = right.key?(:and) ? 'and' : 'or'
50
+ node = build_conjunction(op, data)
51
+ end
52
+ node
53
+ end
54
+
55
+ def self.build_domain_attrs(domain_attr)
56
+ attr_list = []
57
+ if domain_attr.is_a?(Hash)
58
+ attr_list << domain_attr[:attr_label].to_s
59
+ else
60
+ domain_attr.each do |label|
61
+ attr_list << label[:attr_label].to_s
62
+ end
63
+ end
64
+ attr_list
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,137 @@
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
+ # search: 'foo.bar filter id = 6 and bar = "foo" order id asc limit 6'
11
+ #
12
+ # filters: 'id = 6 or id = 8 and id = 9'
13
+ # filters: [
14
+ # 'id = 6',
15
+ # {or: 'id = 8'}
16
+ # {and: id = 9'}
17
+ # ]
18
+ #
19
+ # order: 'foo.bar.id asc'
20
+ # order: 'foo.bar.id'
21
+ # order: [
22
+ # 'foo.bar.id',
23
+ # {desc: 'foo.bar.id'},
24
+ # {asc: 'foo.bar.id'}
25
+ # ]
26
+ # limit: 1
27
+ #
28
+ # settings:
29
+ # page: 1
30
+ # per_page: 2
31
+ # disable_pagination
32
+ # first
33
+ # all
34
+ # last
35
+ # error_on_empty
36
+ # parser
37
+ # transform
38
+ #
39
+ class SearchCriteria < BaseCriteria
40
+
41
+ # Parse out the domain into feature, domain, determine the name of the
42
+ # repo this criteria is for and initailize basic settings.
43
+ # global.user
44
+ #
45
+ # membership.user
46
+ # foo.id filter name like "foo" order foo.bar.id asc limit 2
47
+ # foo.id exists foo.id = 5
48
+ #
49
+ # @example
50
+ # SpCore::Domain::Criteria('foo', single: true)
51
+ # Types.Criteria('foo.bar', single: true)
52
+ #
53
+ # === Options
54
+ # error_on_empty: will cause the repo to fail when query returns an
55
+ # an empty dataset. The failure will have the message
56
+ # with key as domain and text is "<domain> not found"
57
+ #
58
+ # single: will cause the repo to return only one, the first,
59
+ # entity in the dataset
60
+ #
61
+ # @param domain [String] fully qualified domain name
62
+ # @param opts [Hash] options for initializing criteria
63
+ # @return [Criteria]
64
+ #
65
+ # @param domain [String] fully qualified domain name
66
+ # @param opts [Hash] options for initializing criteria
67
+ # @return [Criteria]
68
+ def initialize(domain_name, data = {})
69
+ super
70
+ @limit = nil
71
+ @order = []
72
+ filter(data[:filter]) if data[:filter]
73
+ end
74
+
75
+ def filter(str, op: 'and')
76
+ expr = parse_expr(str)
77
+ return false unless expr
78
+
79
+ expr = qualify_expr(expr)
80
+ if filters?
81
+ expr = ExprConjunction.new(op, filters, expr)
82
+ end
83
+
84
+ @filters = filter
85
+ self
86
+ end
87
+
88
+ def limit(nbr = nil)
89
+ return @limit if nbr.nil?
90
+
91
+ @limit = Integer(nbr)
92
+ fail "limit must be an integer greater than 0" unless nbr > 0
93
+ self
94
+ end
95
+
96
+ # order first_name asc, last_name
97
+ # order: 'foo.bar.id asc'
98
+ # order: 'foo.bar.id'
99
+ # order: [
100
+ # 'foo.bar.id',
101
+ # {desc: 'foo.bar.id'},
102
+ # {asc: 'foo.bar.id'}
103
+ # ]
104
+ #
105
+ # membership.user.id
106
+ def order(data)
107
+ data = [data] if data.is_a?(String)
108
+ unless data.respond_to?(:each)
109
+ fail "order must be a string or implement :each"
110
+ end
111
+ data.each do |item|
112
+ item = transform_order_string(item) if item.is_a?(String)
113
+
114
+ if !item.is_a?(Hash)
115
+ fail "order array must be a list of strings or hashes"
116
+ end
117
+
118
+ dir, domain_attr = item.first
119
+ dir = dir.to_s.downcase
120
+ unless ['desc', 'asc'].include(dir)
121
+ fail "order array item must have a hash key of desc or asc"
122
+ end
123
+ @order << {dir => domain_attr}
124
+ end
125
+ self
126
+ end
127
+
128
+ private
129
+
130
+ def transform_order_string(str)
131
+ str, dir = item.split(' ')
132
+ dir = 'asc' if dir.nil?
133
+ {dir.downcase => str}
134
+ end
135
+ end
136
+ end
137
+ end
@@ -1,7 +1,12 @@
1
1
  require_relative "domain/domain_name_parser"
2
2
  require_relative "domain/dsl"
3
3
  require_relative "domain/expr"
4
+ require_relative "domain/expr_conjunction"
4
5
  require_relative "domain/entity"
5
6
  require_relative "domain/value_object"
6
7
  require_relative "domain/entity_collection"
7
- require_relative "domain/criteria"
8
+ require_relative "domain/criteria_settings"
9
+ require_relative "domain/base_criteria"
10
+ require_relative "domain/search_criteria"
11
+ require_relative "domain/exists_criteria"
12
+ require_relative "domain/expr_parser"
@@ -22,6 +22,11 @@ module Appfuel
22
22
  require "#{container[:features_path]}/#{name}"
23
23
  end
24
24
 
25
+ container[:auto_register_classes].each do |klass|
26
+ next unless klass.register?
27
+ container.register(klass.container_class_path, klass)
28
+ end
29
+
25
30
  return false if initialized?(container, feature_key)
26
31
 
27
32
  Appfuel.run_initializers(feature_key, container)
@@ -2,6 +2,9 @@ module Appfuel
2
2
  module Handler
3
3
  class Action < Base
4
4
  class << self
5
+ def container_class_type
6
+ 'actions'
7
+ end
5
8
 
6
9
  # In order to reduce the length of namespaces actions are not required
7
10
  # to be inside an Actions namespace, but, it is namespaced with in the
@@ -14,7 +14,7 @@ module Appfuel
14
14
  # @param klass [Class] the handler class that is inheriting this
15
15
  # @return nil
16
16
  def inherited(klass)
17
- register_container_class(klass)
17
+ stage_class_for_registration(klass)
18
18
  end
19
19
 
20
20
  def response_handler
@@ -83,6 +83,16 @@ module Appfuel
83
83
  self.class.error(*args)
84
84
  end
85
85
 
86
+ def search(repo, domain, options = {})
87
+ criteria = build_criteria(domain, options)
88
+ repo = data[repo]
89
+ repo.search(criteria)
90
+ end
91
+
92
+ def build_criteria(domain, options)
93
+
94
+ end
95
+
86
96
  def present(name, data, inputs = {})
87
97
  return data if inputs[:raw] == true
88
98
 
@@ -2,6 +2,10 @@ module Appfuel
2
2
  module Handler
3
3
  class Command < Base
4
4
  class << self
5
+ def container_class_type
6
+ 'commands'
7
+ end
8
+
5
9
  def resolve_dependencies(results = Dry::Container.new)
6
10
  =begin
7
11
  super
@@ -5,8 +5,13 @@ module Appfuel
5
5
 
6
6
  class << self
7
7
  attr_writer :mapper
8
+
9
+ def container_class_type
10
+ 'repositories'
11
+ end
12
+
8
13
  def inherited(klass)
9
- register_container_class(klass)
14
+ stage_class_for_registration(klass)
10
15
  end
11
16
 
12
17
  def mapper
@@ -14,7 +19,7 @@ module Appfuel
14
19
  end
15
20
 
16
21
  def create_mapper(maps = nil)
17
- Mapper.new(maps)
22
+ Mapper.new(container_root_name, maps)
18
23
  end
19
24
  end
20
25
 
@@ -22,6 +27,15 @@ module Appfuel
22
27
  self.class.mapper
23
28
  end
24
29
 
30
+ def search(criteria)
31
+ mapper.search(criteria)
32
+ end
33
+
34
+ def exists?(criteria)
35
+ expr = criteria.fiilters
36
+ mapper.exists?(expr)
37
+ end
38
+
25
39
  def to_storage(entity, exclude = [])
26
40
  mapper.to_storage(entity, exclude)
27
41
  end
@@ -118,9 +118,12 @@ module Appfuel
118
118
  # @param entity [String] name of the entity
119
119
  # @param attr [String] name of the attribute
120
120
  # @return [Object]
121
- def storage_class(domain_name, domain_attr, type)
122
- entry = find(domain_name, attr)
121
+ def storage_class(entity_name, entity_attr, type)
122
+ entry = find(entity_name, entity_attr)
123
+ storage_class_from_entry(entry, type)
124
+ end
123
125
 
126
+ def storage_class_from_entry(entry, type)
124
127
  unless entry.storage?(type)
125
128
  fail "No (#{type}) storage has been mapped"
126
129
  end
@@ -128,26 +131,57 @@ module Appfuel
128
131
  container_name = entry.container_name
129
132
  unless container_root_name == container_name
130
133
  fail "You can not access a mapping outside of this container " +
131
- "(#{container_root_name}, #{container_name})"
134
+ "(mapper: #{container_root_name}, entry: #{container_name})"
132
135
  end
133
- app_container = Appfuel.app_container(entry.container)
136
+ app_container = Appfuel.app_container(entry.container_name)
134
137
  key = entry.storage(type)
135
138
  app_container[key]
136
139
  end
137
140
 
138
- def to_entity_hash(domain_name, data, opts = {})
141
+ def to_entity_hash(domain_name, data)
139
142
  entity_attrs = {}
140
143
  each_entity_attr(domain_name) do |entry|
141
144
  attr_name = entry.storage_attr
142
145
  domain_attr = entry.domain_attr
143
146
  next unless data.key?(attr_name)
144
-
145
147
  update_entity_hash(domain_attr, data[attr_name], entity_attrs)
146
148
  end
147
149
 
148
150
  entity_attrs
149
151
  end
150
152
 
153
+ # Convert the domain into a hash of storage attributes that represent.
154
+ # Each storage class has its own hash of mapped attributes. A domain
155
+ # can have more than one storage class.
156
+ #
157
+ # @param domain [Appfuel::Domain::Entity]
158
+ # @param type [Symbol] type of storage :db, :file, :memory etc...
159
+ # @param opts [Hash]
160
+ # @option exclued [Array] list of columns to exclude from mapping
161
+ #
162
+ # @return [Hash] each key is a storage class with a hash of column
163
+ # name/value
164
+ def to_storage(domain, type, opts = {})
165
+ unless domain.respond_to?(:domain_name)
166
+ fail "Domain entity must implement :domain_name"
167
+ end
168
+
169
+ excluded = opts[:exclude] || []
170
+ data = {}
171
+ each_entity_attr(domain.domain_name) do |entry|
172
+ unless entry.storage?(type)
173
+ "storage type (#{type}) is not support for (#{domain.domain_name})"
174
+ end
175
+ storage_attr = entry.storage_attr
176
+ storage_class = entry.storage(type)
177
+ next if excluded.include?(storage_attr) || entry.skip?
178
+
179
+ data[storage_class] = {} unless data.key?(storage_class)
180
+ data[storage_class][storage_attr] = entity_value(domain, entry)
181
+ end
182
+ data
183
+ end
184
+
151
185
  def update_entity_hash(domain_attr, value, hash)
152
186
  if domain_attr.include?('.')
153
187
  hash.deep_merge!(create_entity_hash(domain_attr, value))
@@ -159,7 +193,7 @@ module Appfuel
159
193
  def entity_value(domain, map_entry)
160
194
  value = resolve_entity_value(domain, map_entry.domain_attr)
161
195
  if map_entry.computed_attr?
162
- value = map_entry.compute_attr(domain, value)
196
+ value = map_entry.computed_attr(value, domain)
163
197
  end
164
198
 
165
199
  value = nil if undefined?(value)
@@ -165,13 +165,13 @@ module Appfuel
165
165
  end
166
166
  end
167
167
 
168
- def initialize_file_storage(value, opts = {})
168
+ def initialize_file_storage(value)
169
169
  key = translate_storage_key(:file, domain_name)
170
170
  case value
171
171
  when true
172
172
  {
173
- model: 'storage.file.model',
174
- path: "#{storage_path}/#{key.gsub(/\./,'/')}.yml"
173
+ model: 'file.model',
174
+ path: "#{storage_path}/#{key.tr('.', '/')}.yml"
175
175
  }
176
176
  end
177
177
  end
@@ -195,7 +195,7 @@ module Appfuel
195
195
 
196
196
  top, *parts = partial_key.split('.')
197
197
  top = "features.#{top}" unless top == 'global'
198
- "#{top}.storage.#{type}.#{parts.join('.')}"
198
+ "#{top}.#{type}.#{parts.join('.')}"
199
199
  end
200
200
 
201
201
  def assign_storage(type, partial_key)
@@ -51,10 +51,10 @@ module Appfuel
51
51
  end
52
52
 
53
53
  def computed_attr?
54
- !computed_attr.nil?
54
+ !@computed_attr.nil?
55
55
  end
56
56
 
57
- def compute_attr(value, domain)
57
+ def computed_attr(value, domain)
58
58
  fail "No lambda assigned to compute value" unless computed_attr?
59
59
  @computed_attr.call(value, domain)
60
60
  end
@@ -2,7 +2,6 @@ require_relative 'repository/base'
2
2
  require_relative 'repository/mapping_entry'
3
3
  require_relative 'repository/mapping_dsl'
4
4
  require_relative 'repository/mapper'
5
- require_relative 'repository/mapping_registry'
6
5
  require_relative 'repository/initializer'
7
6
 
8
7
  module Appfuel
@@ -1,41 +1,45 @@
1
1
  module Appfuel
2
2
  module Db
3
+ # ActiveRecord::Base that auto registers itself into the application
4
+ # container and symbolizes its attributes. This is used by the db
5
+ # mapper to persist and retreive domains to and from the database.
6
+ #
7
+ # NOTE: we are coupling ourselves to active record right now. I have
8
+ # plans to resolve this but right now its a lower priority. You
9
+ # can get around this by implementing your own mapper, and db model.
10
+ #
3
11
  class ActiveRecordModel < ActiveRecord::Base
4
- # ChangeOrder::Global::Db::FooBar
5
- #
6
- # ChangeOrder::Membership::Peristence::Db::Account
7
- # ChangeOrder::Membership::Persistence::Yaml::Account
8
- #
9
- # ChangeOrder::Membership::Domains::Account
10
- #
11
- # Appfuel.mapping membership.account,
12
- # db: account, yaml: account do
13
- # map id, account.id
14
- # end
15
- #
16
- # module Membership
17
- # module Db
18
- #
19
- # end
20
- # end
21
- #
22
- #
23
- # global.db.foobar
12
+ include Appfuel::Application::AppContainer
13
+
14
+ self.abstract_class = true
15
+
16
+ # Contributes to the construction of a fully qualified container key
24
17
  #
25
- # features.membership.db.account
26
- # features.membership.yaml.account
18
+ # @example
19
+ # global model: global.db.user
20
+ # feature model: features.membership.db.user
27
21
  #
22
+ # @return [String]
23
+ def self.container_class_type
24
+ 'db'
25
+ end
26
+
27
+ # Registers the class inside the application container. The class
28
+ # being registered as the mixin required for registration.
28
29
  #
29
- self.abstract_class = true
30
- include Appfuel::Application::AppContainer
30
+ # @params klass [Class] class that is inheriting this one
31
+ # @return results from super
31
32
  def self.inherited(klass)
33
+ stage_class_for_registration(klass)
32
34
  super
33
- register_container_class(klass)
34
35
  end
35
36
 
36
-
37
- def entity_attributes
38
- attributes.symbolize_keys.select {|_,value| !value.nil?}
37
+ # Symbolize active record attributes and remove attributes with
38
+ # nil values
39
+ #
40
+ # @return [Hash]
41
+ def domain_attrs
42
+ attributes.symbolize_keys.select {|_k, v| !v.nil? }
39
43
  end
40
44
  end
41
45
  end