appfuel 0.2.0

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 (84) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +25 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +19 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +9 -0
  9. data/README.md +38 -0
  10. data/Rakefile +6 -0
  11. data/appfuel.gemspec +42 -0
  12. data/bin/console +7 -0
  13. data/bin/setup +8 -0
  14. data/lib/appfuel.rb +210 -0
  15. data/lib/appfuel/application.rb +4 -0
  16. data/lib/appfuel/application/app_container.rb +223 -0
  17. data/lib/appfuel/application/container_class_registration.rb +22 -0
  18. data/lib/appfuel/application/container_key.rb +201 -0
  19. data/lib/appfuel/application/qualify_container_key.rb +76 -0
  20. data/lib/appfuel/application/root.rb +140 -0
  21. data/lib/appfuel/cli_msg_request.rb +19 -0
  22. data/lib/appfuel/configuration.rb +14 -0
  23. data/lib/appfuel/configuration/definition_dsl.rb +175 -0
  24. data/lib/appfuel/configuration/file_loader.rb +61 -0
  25. data/lib/appfuel/configuration/populate.rb +95 -0
  26. data/lib/appfuel/configuration/search.rb +45 -0
  27. data/lib/appfuel/db_model.rb +16 -0
  28. data/lib/appfuel/domain.rb +7 -0
  29. data/lib/appfuel/domain/criteria.rb +436 -0
  30. data/lib/appfuel/domain/domain_name_parser.rb +44 -0
  31. data/lib/appfuel/domain/dsl.rb +247 -0
  32. data/lib/appfuel/domain/entity.rb +242 -0
  33. data/lib/appfuel/domain/entity_collection.rb +87 -0
  34. data/lib/appfuel/domain/expr.rb +127 -0
  35. data/lib/appfuel/domain/value_object.rb +7 -0
  36. data/lib/appfuel/errors.rb +104 -0
  37. data/lib/appfuel/feature.rb +2 -0
  38. data/lib/appfuel/feature/action_loader.rb +25 -0
  39. data/lib/appfuel/feature/initializer.rb +43 -0
  40. data/lib/appfuel/handler.rb +6 -0
  41. data/lib/appfuel/handler/action.rb +17 -0
  42. data/lib/appfuel/handler/base.rb +103 -0
  43. data/lib/appfuel/handler/command.rb +18 -0
  44. data/lib/appfuel/handler/inject_dsl.rb +88 -0
  45. data/lib/appfuel/handler/validator_dsl.rb +256 -0
  46. data/lib/appfuel/initialize.rb +70 -0
  47. data/lib/appfuel/initialize/initializer.rb +68 -0
  48. data/lib/appfuel/msg_request.rb +207 -0
  49. data/lib/appfuel/predicates.rb +10 -0
  50. data/lib/appfuel/presenter.rb +18 -0
  51. data/lib/appfuel/presenter/base.rb +7 -0
  52. data/lib/appfuel/repository.rb +73 -0
  53. data/lib/appfuel/repository/base.rb +72 -0
  54. data/lib/appfuel/repository/initializer.rb +19 -0
  55. data/lib/appfuel/repository/mapper.rb +203 -0
  56. data/lib/appfuel/repository/mapping_dsl.rb +210 -0
  57. data/lib/appfuel/repository/mapping_entry.rb +76 -0
  58. data/lib/appfuel/repository/mapping_registry.rb +121 -0
  59. data/lib/appfuel/repository_runner.rb +60 -0
  60. data/lib/appfuel/request.rb +53 -0
  61. data/lib/appfuel/response.rb +96 -0
  62. data/lib/appfuel/response_handler.rb +79 -0
  63. data/lib/appfuel/root_module.rb +31 -0
  64. data/lib/appfuel/run_error.rb +9 -0
  65. data/lib/appfuel/storage.rb +3 -0
  66. data/lib/appfuel/storage/db.rb +4 -0
  67. data/lib/appfuel/storage/db/active_record_model.rb +42 -0
  68. data/lib/appfuel/storage/db/mapper.rb +213 -0
  69. data/lib/appfuel/storage/db/migration_initializer.rb +42 -0
  70. data/lib/appfuel/storage/db/migration_runner.rb +15 -0
  71. data/lib/appfuel/storage/db/migration_tasks.rb +18 -0
  72. data/lib/appfuel/storage/db/repository.rb +231 -0
  73. data/lib/appfuel/storage/db/repository_query.rb +13 -0
  74. data/lib/appfuel/storage/file.rb +1 -0
  75. data/lib/appfuel/storage/file/base.rb +32 -0
  76. data/lib/appfuel/storage/memory.rb +2 -0
  77. data/lib/appfuel/storage/memory/mapper.rb +30 -0
  78. data/lib/appfuel/storage/memory/repository.rb +37 -0
  79. data/lib/appfuel/types.rb +53 -0
  80. data/lib/appfuel/validation.rb +80 -0
  81. data/lib/appfuel/validation/validator.rb +59 -0
  82. data/lib/appfuel/validation/validator_pipe.rb +47 -0
  83. data/lib/appfuel/version.rb +3 -0
  84. metadata +335 -0
@@ -0,0 +1,53 @@
1
+ module Appfuel
2
+ class Request
3
+ attr_reader :action_route, :feature, :action, :inputs, :namespace
4
+
5
+ def initialize(action_route, inputs = {})
6
+ unless inputs.respond_to?(:to_h)
7
+ fail "inputs must respond to :to_h"
8
+ end
9
+ @inputs = inputs.to_h
10
+ @action_route, @feature, @action = parse_route(action_route)
11
+ @namespace = "features.#{feature}.actions.#{action}"
12
+ end
13
+
14
+ private
15
+
16
+ # The service route is a forward slash separated string consisting of two
17
+ # parts. The first part is the feature that holds the action and the
18
+ # second is the action itself.
19
+ #
20
+ # @example 'offers/create'
21
+ # feature is Offers
22
+ # action is Create
23
+ #
24
+ # @param route [String]
25
+ # @return [Array]
26
+ def parse_route(route)
27
+ feature_name, action_name = route.to_s.split('/')
28
+
29
+ feature_name = handle_parsed_string(feature_name)
30
+ action_name = handle_parsed_string(action_name)
31
+
32
+ handle_empty_feature(feature_name)
33
+ handle_empty_action(action_name)
34
+
35
+
36
+ [route, feature_name, action_name]
37
+ end
38
+
39
+ def handle_parsed_string(value)
40
+ value.to_s.strip
41
+ end
42
+
43
+ def handle_empty_feature(feature_name)
44
+ return unless feature_name.empty?
45
+ fail "feature is missing, action route must be like <feature/action>"
46
+ end
47
+
48
+ def handle_empty_action(action_name)
49
+ return unless action_name.empty?
50
+ fail "action is missing, action route must be like <feature/action>"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,96 @@
1
+ module Appfuel
2
+ # Every action or command must return a response. A response is either
3
+ # ok or it has errors. You can retrieve the results with the "ok" method
4
+ # or the errors with the "error" method
5
+ class Response
6
+
7
+ class << self
8
+ # Convience method for creating a successfull response
9
+ #
10
+ # @param result Hash the successfull resultset
11
+ # @reuturn Response
12
+ def ok(result = nil)
13
+ self.new(ok: result)
14
+ end
15
+
16
+ # Convience method for creating an error response. It understands
17
+ # how to handle a SpCore::Error object. Any thing that
18
+ # is not a hash or can't be converted to a hash is assumed to be
19
+ # a string and converted into a general_error
20
+ #
21
+ # @param data Hash the errors hash
22
+ # @reuturn Response
23
+ def error(data)
24
+ result = format_result_hash(data, default_key: :general_error)
25
+ result = result[:errors] if result.key?(:errors)
26
+ self.new(errors: result)
27
+ end
28
+
29
+ def format_result_hash(data, default_key:)
30
+ if data.is_a?(Hash)
31
+ result = data
32
+ elsif data.respond_to?(:to_h)
33
+ result = data.to_h
34
+ else
35
+ result = {default_key => data.to_s}
36
+ end
37
+
38
+ result.symbolize_keys
39
+ end
40
+ end
41
+
42
+ attr_reader :ok, :errors
43
+
44
+ # @param data [Hash]
45
+ # @return [Response]
46
+ def initialize(data = {})
47
+ result = format_result_hash(data)
48
+
49
+ # when no ok key and no errors key the assume
50
+ # it is a successfull response
51
+ if !result.key?(:ok) && !result.key?(:errors)
52
+ result = {ok: result}
53
+ end
54
+
55
+ @ok = result[:ok]
56
+ @errors = nil
57
+ if result.key?(:errors)
58
+ @ok = nil
59
+ @errors = Errors.new(result[:errors])
60
+ end
61
+ end
62
+
63
+ def errors?
64
+ !ok?
65
+ end
66
+ alias_method :failure?, :errors?
67
+
68
+ def error_messages
69
+ return {} if ok?
70
+
71
+ errors.messages
72
+ end
73
+
74
+ def ok?
75
+ errors.nil?
76
+ end
77
+ alias_method :success?, :ok?
78
+
79
+ def to_h
80
+ if ok?
81
+ {ok: ok}
82
+ else
83
+ errors.to_h
84
+ end
85
+ end
86
+
87
+ def to_json
88
+ to_h.to_json
89
+ end
90
+
91
+ private
92
+ def format_result_hash(data)
93
+ self.class.format_result_hash(data, default_key: :ok)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,79 @@
1
+ module Appfuel
2
+ class ResponseHandler
3
+ attr_reader :response_class
4
+
5
+ def initialize(response_class = Response)
6
+ @response_class = response_class
7
+ end
8
+
9
+ def create_response(data)
10
+ return data if response?(data)
11
+ return error(data) if error_data?(data)
12
+ ok(data)
13
+ end
14
+
15
+ def response?(data)
16
+ data.is_a?(response_class)
17
+ end
18
+
19
+ # Determine if the data given is an error by looking at its class or
20
+ # checking if it is a hash with the key :errors
21
+ #
22
+ # @param data
23
+ # @return Bool
24
+ def error_data?(data)
25
+ case
26
+ when data.kind_of?(::StandardError) || data.is_a?(Errors)
27
+ true
28
+ when data.is_a?(Hash)
29
+ data.key?(:errors)
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ # This is used when returning results back to the action handler. We
36
+ # use this to indicate it was a successful response
37
+ #
38
+ # @param ok Hash
39
+ # @return Response
40
+ def ok(value = nil)
41
+ response_class.ok(value)
42
+ end
43
+
44
+ # Convert a number of different error formats into hash and use that to
45
+ # build the response
46
+ #
47
+ # @param args StandardError|ActiveModel::Errors|Hash|Errors|Symbol|Response
48
+ # @return Response
49
+ def error(*args)
50
+ error = args.shift
51
+ case
52
+ when error.kind_of?(ActiveModel::Errors)
53
+ messages = error.messages
54
+ when error.kind_of?(StandardError)
55
+ key = error.class.to_s.underscore.to_sym
56
+ backtrace_key = "#{key}_backtrace".to_sym
57
+ messages = {
58
+ errors: {
59
+ key => [error.message],
60
+ backtrace_key => error.backtrace || []
61
+ }
62
+ }
63
+ when error.is_a?(Hash)
64
+ messages = error.key?(:errors) ? error : {errors: error}
65
+ when error.is_a?(Errors)
66
+ messages = error.to_h
67
+
68
+ when args.length >= 1
69
+ messages = {errors: {error => args}}
70
+ when error.is_a?(response_class)
71
+ return error
72
+ else
73
+ messages = {errors: {general_error: [error.to_s]}}
74
+ end
75
+
76
+ response_class.error(messages)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,31 @@
1
+ module Appfuel
2
+ # The root module is an import concept. It represents the services top most
3
+ # namespace. It is assumed that the root module will have a feature module,
4
+ # its child, and that feature module will have many action classes inside it.
5
+ module RootModule
6
+
7
+ def root_module=(value)
8
+ fail "Root module must be a module" unless value.is_a?(Module)
9
+ @root_module = value
10
+ end
11
+
12
+ def root_module
13
+ @root_module ||= root_module_const
14
+ end
15
+
16
+ protected
17
+
18
+ def root_module_const
19
+ name = root_module_name
20
+ unless Kernel.const_defined?(name)
21
+ fail "Root module is not defined (#{name})"
22
+ end
23
+
24
+ Kernel.const_get(name)
25
+ end
26
+
27
+ def root_module_name
28
+ self.to_s.split("::").first
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ module Appfuel
2
+ class RunError < StandardError
3
+ attr_reader :response
4
+
5
+ def initialize(response)
6
+ @response = response
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'storage/file'
2
+ require_relative 'storage/memory'
3
+ require_relative 'storage/db'
@@ -0,0 +1,4 @@
1
+ require_relative 'db/mapper'
2
+ require_relative 'db/repository_query'
3
+ require_relative 'db/repository'
4
+ require_relative 'db/active_record_model'
@@ -0,0 +1,42 @@
1
+ module Appfuel
2
+ module Db
3
+ 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
24
+ #
25
+ # features.membership.db.account
26
+ # features.membership.yaml.account
27
+ #
28
+ #
29
+ self.abstract_class = true
30
+ include Appfuel::Application::AppContainer
31
+ def self.inherited(klass)
32
+ super
33
+ register_container_class(klass)
34
+ end
35
+
36
+
37
+ def entity_attributes
38
+ attributes.symbolize_keys.select {|_,value| !value.nil?}
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,213 @@
1
+ module Appfuel
2
+ module Db
3
+ class Mapper < Appfuel::Repository::Mapper
4
+
5
+ # Determines if an domain entity exists for this key
6
+ #
7
+ # @param key [String, Symbol]
8
+ # @return [Boolean]
9
+ def entity_mapped?(name)
10
+ registry.entity?(name)
11
+ end
12
+
13
+ # Returns the active record model from a map for a given entity
14
+ #
15
+ # @raise [RuntimeError] if entity key does not exist
16
+ # @raise [RuntimeError] if map key does not exist
17
+ #
18
+ # @param entity [String] encoded "feature.entity"
19
+ # @param domain_attr [String] attribute of entity
20
+ # @return [DbModel]
21
+ def db_class_key(entity_name, entity_attr)
22
+ entry = find(entity_name, entity_attr)
23
+ db_class_key = entry.storage(:db)
24
+
25
+ mapp.storage(entity, domain_attr)
26
+ end
27
+
28
+ # Converts an entity expression into a valid active record expresion with
29
+ # string expresion (array canditions) and value(s)
30
+ #
31
+ # @param expr [Domain::Expr]
32
+ # @param results [Hash]
33
+ # @return [DbExpr] Returns a valid active record expresion
34
+ def create_db_expr(expr)
35
+ DbExpr.new(qualified_db_column(expr), expr.op, expr.value)
36
+ end
37
+
38
+ # Validates if a record exists in the table that matches the array with
39
+ # the conditions given.
40
+ #
41
+ # @param criteria [Criteria]
42
+ # @return [Boolean]
43
+ def exists?(criteria)
44
+ domain_expr = criteria.exists_expr
45
+ domain_name = domain_expr.domain_name
46
+ domain_attr = domain_expr.domain_attr
47
+
48
+ db_expr = create_db_expr(domain_expr)
49
+ db_model = registry.db_class(domain_name, domain_attr)
50
+ db_model.exists?([db_expr.string, db_expr.values])
51
+ end
52
+
53
+ # Build a where expression from the mapped db class using the criteria.Ï
54
+ #
55
+ # @param criteria [Criteria]
56
+ # @param relation [DbModel, ActiveRecord::Relation]
57
+ # @return [DbModel, ActiveRecord::Relation]
58
+ def where(criteria, relation)
59
+ unless criteria.where?
60
+ fail "you must explicitly call :all when criteria has no exprs."
61
+ end
62
+
63
+ criteria.each do |domain_expr, op|
64
+ relation = if op == :or
65
+ relation.or(db_where(domain_expr, relation))
66
+ else
67
+ db_where(domain_expr, relation)
68
+ end
69
+ end
70
+ relation
71
+ end
72
+
73
+ # Return qualified db column name from entity expression.
74
+ #
75
+ # @param expr [SpCore::Domain::Expr]
76
+ # @return db column name [String]
77
+ def qualified_db_column(expr)
78
+ table_name, column = db_table_column(expr)
79
+ "#{table_name}.#{column}"
80
+ end
81
+
82
+ # Determine Domain Mapentry and DbModel from entity expression.
83
+ #
84
+ # @param expr [SpCore::Domain::Expr]
85
+ # @return [table_name, column] [Array]
86
+ def db_table_column(expr)
87
+ entry = registry.find(expr.domain_name, expr.domain_attr)
88
+ db = registry.db_class_constant(entry.db_class)
89
+ [db.table_name, entry.db_column]
90
+ end
91
+
92
+ # Build an order by expression for the given db relation based on the
93
+ # criteria
94
+ #
95
+ # @param criteria [Criteria]
96
+ # @param relation [DbModel, ActiveRecord::Relation]
97
+ # @return [ActiveRecord::Relation]
98
+ def order(criteria, relation)
99
+ return relation unless criteria.order?
100
+ criteria.order.each do |expr|
101
+ db_column = qualified_db_column(expr)
102
+ direction = expr.value
103
+ relation = relation.order("#{db_column} #{direction}")
104
+ end
105
+ relation
106
+ end
107
+
108
+ # Eventhough there is no mapping here we add the interface for
109
+ # consistency.
110
+ #
111
+ # @param criteria [Criteria]
112
+ # @param relation [DbModel, ActiveRecord::Relation]
113
+ # @return [ActiveRecord::Relation]
114
+ def limit(criteria, relation)
115
+ return relation unless criteria.limit?
116
+
117
+ relation.limit(criteria.limit)
118
+ end
119
+
120
+ # Map the entity expr to a hash of db_column => value and call
121
+ # on the relation using that.
122
+ #
123
+ # @note this is db library specific and needs to be moved to an adapter
124
+ #
125
+ # @param expr [Appfuel::Domain::Expr]
126
+ # @param relation [ActiveRecord::Relation]
127
+ # @return [ActiveRecord::Relation]
128
+ def db_where(domain_expr, relation)
129
+ db_expr = create_db_expr(domain_expr)
130
+ relation.where([db_expr.string, db_expr.values])
131
+ end
132
+
133
+ # Convert the entity into a hash of db tables that represent
134
+ # that entity. Each table has its own hash of mapped columns.
135
+ #
136
+ # @param domain [Appfuel::Domain::Entity]
137
+ # @param opts [Hash]
138
+ # @option exclued [Array] list of columns to exclude from mapping
139
+ #
140
+ # @return [Hash] each key is a table with a hash of column name/value
141
+ def to_storage(domain, opts = {})
142
+ excluded = opts[:exclude] || []
143
+ data = {}
144
+ each_entity_attr(domain.domain_name) do |entry|
145
+ column = entry.storage_attr
146
+ db_class = entry.storage(:db)
147
+ next if excluded.include?(column) || entry.skip?
148
+
149
+ data[db_class] = {} unless data.key?(db_class)
150
+ data[db_class][column] = entity_value(domain, entry)
151
+ end
152
+ data
153
+ end
154
+
155
+ # Handles entity value by checking if its a computed property,
156
+ # fetching the value and converting undefined values to nil.
157
+ #
158
+ # @param domain [Appfuel::Domain::Entity]
159
+ # @param map_entry [MappingEntity]
160
+ # @return the value
161
+ def entity_value(domain, entry)
162
+ value = retrieve_entity_value(domain, entry.domain_attr)
163
+ if entry.computed_attr?
164
+ value = entry.compute_attr(value)
165
+ end
166
+
167
+ value = nil if undefined?(value)
168
+
169
+ value
170
+ end
171
+
172
+ # @params value [mixed]
173
+ # @return [Boolean]
174
+ def undefined?(value)
175
+ value == Types::Undefined
176
+ end
177
+
178
+ # Fetch the value for the entity attribute. When the attribute name
179
+ # contains a '.' then traverse the dots and call the last attribute
180
+ # for the value
181
+ #
182
+ # @param domain [Appfuel::Domain::Entity]
183
+ # @param entity_attribute [String]
184
+ # @return the value
185
+ def retrieve_entity_value(domain, entity_attr)
186
+ chain = entity_attr.split('.')
187
+ target = domain
188
+ chain.each do |attr_method|
189
+ unless target.respond_to?(attr_method)
190
+ return nil
191
+ end
192
+
193
+ target = target.public_send(attr_method)
194
+ end
195
+ target
196
+ end
197
+
198
+ # Create nested hashes from string
199
+ #
200
+ # @param domain_attr [String]
201
+ # @param entity_value [String]
202
+ # @return [nested hash]
203
+ def create_entity_hash(domain_attr, entity_value)
204
+ domain_attr.split('.').reverse.inject(entity_value) { |a,n| {n => a}}
205
+ end
206
+
207
+ def model_attributes(relation)
208
+ ap relation
209
+ relation.attributes.select {|_, value| !value.nil?}
210
+ end
211
+ end
212
+ end
213
+ end