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