appfuel 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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,207 @@
1
+ module Appfuel
2
+ # This represents the message delivered by RabbitMQ. We encapsulate it
3
+ # so that if you want to fire an action from the command line you can
4
+ # use a CliRequest and not worry about rabbit details
5
+ #
6
+ class MsgRequest
7
+ attr_reader :config, :service_route, :reply_to, :correlation_id,
8
+ :delivery_info, :properties, :feature, :action, :inputs, :current_user
9
+
10
+ #
11
+ # metadata properties
12
+ # headers: message headers, important for service_route
13
+ # reply_to: name of rpc response queue
14
+ # correlation_id: id used in rpc to match response
15
+ #
16
+ # @param msg String serialized message from rabbitmq
17
+ # @param delivery_info Hash info used to acknowledge messages
18
+ # @param metadata Object properties of the messages
19
+ #
20
+ # @return MsgRequest
21
+ def initialize(msg, delivery_info, metadata)
22
+ @auditable = true
23
+ self.inputs = msg
24
+ self.delivery_info = delivery_info
25
+ self.properties = metadata
26
+ end
27
+
28
+ # Rpc requires a reply queue to respond to and a correlation_id to
29
+ # identify that response in the queue. When these two things exist
30
+ # then the request is consided to be an rpc
31
+ #
32
+ # @return [Boolean]
33
+ def rpc?
34
+ !reply_to.nil? && !correlation_id.nil?
35
+ end
36
+
37
+ # Flag used to determine if the request should be sent to the audit log.
38
+ # The default value is true and a header is used to opt out of this.
39
+ #
40
+ # @return [Boolean]
41
+ def auditable?
42
+ @auditable
43
+ end
44
+
45
+ # The current user is required for all audit logs and this flag is used
46
+ # to determine if it exists. When an audit is not required the current
47
+ # user is optional
48
+ #
49
+ # @return [Boolean]
50
+ def current_user?
51
+ !@current_user.nil?
52
+ end
53
+
54
+ private
55
+
56
+ # Ensures when a current_user_id is given it is valid. This is optional
57
+ # and the msg will not enforce its presence when auditable is true, that
58
+ # will be handled by objects implementing auditing.
59
+ #
60
+ # @param value [Integer, nil] the current user id
61
+ # @return [Integer, nil]
62
+ def current_user=(value)
63
+ return @current_user = nil if value.nil?
64
+
65
+ begin
66
+ @current_user = Integer(value)
67
+ rescue
68
+ raise 'current_user_id must be an Integer'
69
+ end
70
+ end
71
+
72
+ # All message inputs are sent encoded as json. We parse then json then
73
+ # symbolize the keys which allows any validation, action or command to
74
+ # expect a consistent hash
75
+ #
76
+ # @param data [String]
77
+ # @return [Hash]
78
+ def inputs=(data)
79
+ data = data.to_s
80
+ return @inputs = {} if data.empty?
81
+
82
+
83
+ begin
84
+ data = JSON.parse(data)
85
+ fail "message inputs must be a hash" unless data.is_a?(Hash)
86
+ @inputs = data.deep_symbolize_keys
87
+ rescue => e
88
+ msg = "message request could not parse the inputs: #{e.message}"
89
+ error = RuntimeError.new(msg)
90
+ error.set_backtrace(e.backtrace)
91
+ raise error
92
+ end
93
+ end
94
+
95
+ # Hash like structure that hold information about the delivery of the message
96
+ # :consumer_tag Each consumer (subscription) has an identifier called a
97
+ # consumer tag. It can be used to unsubscribe from
98
+ # messages. Consumer tags are just strings.
99
+ #
100
+ # :delivery_tag If set to 1, the delivery tag is treated as
101
+ # "up to and including", so that multiple messages can be
102
+ # acknowledged with a single method. If set to zero, the
103
+ # delivery tag refers to a single message. If the multiple
104
+ # field is 1, and the delivery tag is zero, this indicates
105
+ # acknowledgement of all outstanding messages.
106
+ #
107
+ # :redelivered true if this delivery is a redelivery ( the message was
108
+ # requeued at least once )
109
+ #
110
+ # :routing_key routing key used by exchange to route to queue
111
+ #
112
+ # :exchange name of exchange
113
+ #
114
+ # :consumer the consumer that subsribed
115
+ #
116
+ # :channel the channel the message was sent on
117
+ #
118
+ # @param data [Bunny::Delivery::Info]
119
+ # @return [Bunny::Delivery::Info]
120
+ def delivery_info=(data)
121
+ @deliver_info = data
122
+ end
123
+
124
+ # Hash like structure that holds attributes of the message as defined by
125
+ # the amqp protocol
126
+ #
127
+ # :content_type (Optional) content type of the message, as set by
128
+ # the publisher
129
+ #
130
+ # :content_encoding (Optional) content encoding of the message, as set
131
+ # by the publisher
132
+ #
133
+ # :headers message headers
134
+ #
135
+ # :delivery_mode [Integer] Delivery mode (persistent or transient)
136
+ #
137
+ # :priority [Integer] Message priority, as set by the publisher
138
+ #
139
+ # :correlation_id [String] What message this message is a reply to
140
+ # (or corresponds to), as set by the publisher
141
+ #
142
+ # :reply_to [String] (Optional) How to reply to the publisher
143
+ # (usually a reply queue name)
144
+ #
145
+ # :expiration [String] Message expiration, as set by the publisher
146
+ #
147
+ # :message_id [String] Message ID, as set by the publisher
148
+ #
149
+ # :timestamp [Time] Message timestamp, as set by the publisher
150
+ #
151
+ # :user_id [String] Publishing user, as set by the publisher
152
+ # not an application user
153
+ #
154
+ # :app_id [String] Publishing application, as set by the
155
+ # publisher
156
+ #
157
+ # :cluster_id [String] Cluster ID, as set by the publisher
158
+ #
159
+ # @param data [Bunny::MessageProperties]
160
+ # @return Bunny::MessageProperties
161
+ def properties=(data)
162
+ @reply_to = data.reply_to
163
+ @correlation_id = data.correlation_id
164
+
165
+ if data.headers['auditable'] == false
166
+ @auditable = false
167
+ end
168
+
169
+ self.service_route = data.headers['service_route']
170
+ self.current_user = data.headers['current_user']
171
+ @properties = data
172
+ end
173
+
174
+ # The service route is a forward slash separated string consisting of two
175
+ # parts. The first part is the feature that holds the action and the
176
+ # second is the action itself.
177
+ #
178
+ # @example 'offers/create'
179
+ # feature is Offers
180
+ # action is Create
181
+ #
182
+ # This is used by the dispatcher to locate the action to be called
183
+ #
184
+ # @param route [String]
185
+ # @param [String]
186
+ def service_route=(route)
187
+ fail "service route missing from message headers" if route.nil?
188
+ fail "service route must be a String" unless route.is_a?(String)
189
+
190
+ feature, action= route.split('/', 2)
191
+
192
+ # NOTE: feature.strip! returns nil we are really after the empty?
193
+ if feature.nil? || (feature.strip! || feature.empty?)
194
+ fail "feature is missing route must be like <feature>/<action>"
195
+ end
196
+
197
+ if action.nil? || (action.strip! || action.empty?)
198
+ fail "action is missing route must be like <feature>/<action>"
199
+ end
200
+
201
+ @service_route = route
202
+ @feature = feature.camelize
203
+ @action = action.camelize
204
+ route
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,10 @@
1
+ module Appfuel
2
+ module Predicates
3
+ include Dry::Logic::Predicates
4
+
5
+ predicate(:criteria?) do |value|
6
+ value.instance_of?(Appfuel::Domain::Criteria)
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'presenter/base'
2
+ module Appfuel
3
+ module Presenter
4
+ def self.present(name, opts = {}, &block)
5
+ key = Appfuel.expand_container_key(name, 'presenters')
6
+ root = opts[:root] || Appfuel.default_app_name
7
+ app_container = Appfuel.app_container(root)
8
+
9
+ presenter = create_presenter(opts[:base_class] || Base, &block)
10
+ app_container.register(key, presenter)
11
+ end
12
+
13
+ def self.create_presenter(klass, &block)
14
+ presenter = klass.new
15
+ ->(data, criteria) { presenter.instance_exec(data, criteria, &block) }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module Appfuel
2
+ module Presenter
3
+ class Base
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,73 @@
1
+ require_relative 'repository/base'
2
+ require_relative 'repository/mapping_entry'
3
+ require_relative 'repository/mapping_dsl'
4
+ require_relative 'repository/mapper'
5
+ require_relative 'repository/mapping_registry'
6
+ require_relative 'repository/initializer'
7
+
8
+ module Appfuel
9
+ module Repository
10
+ # Mapping uses the map_dsl_class to define and map mapping entries
11
+ # into the mapping registry
12
+ #
13
+ # @example Simple mapping
14
+ # mapping 'foo.bar', db: foo_table_one do
15
+ # map 'id'
16
+ # map 'project_user_id', 'user.id'
17
+ # end
18
+ #
19
+ # Note: When no :key value is given to options then the entity base
20
+ # name is used. The following would be equivalent:
21
+ #
22
+ # mapping 'offers.offer', db: foo_table_two do
23
+ # ...
24
+ # end
25
+ #
26
+ # @param entity_name [String] domain name of the entity we are mapping
27
+ # @param db_class [String] name of the database class used in mapping
28
+ # @return [DbEntityMapper]
29
+ def self.mapping(domain_name, options = {}, &block)
30
+ dsl = MappingDsl.new(domain_name, options)
31
+ dsl.instance_eval(&block)
32
+
33
+ dsl.entries.each do |entry|
34
+ root = entry.container_name || Appfuel.default_app_name
35
+ container = Appfuel.app_container(root)
36
+ mappings = container['repository_mappings']
37
+
38
+ domain_name = entry.domain_name
39
+ mappings[domain_name] = {} unless mappings.key?(domain_name)
40
+
41
+ entries = mappings[domain_name]
42
+ entries[entry.domain_attr] = entry
43
+ end
44
+ end
45
+
46
+ def self.entity_builder(domain_name, type, opts = {}, &block)
47
+ fail "entity builder must be used with a block" unless block_given?
48
+
49
+ root = opts[:root] || Appfuel.default_app_name
50
+ repo = create_repo(type, domain_name)
51
+ repo.class.load_path_from_container_namespace("#{root}.#{domain_name}")
52
+
53
+ app_container = Appfuel.app_container(root)
54
+ category = "domain_builders.#{type}"
55
+ builder_key = repo.qualify_container_key(domain_name, category)
56
+ app_container.register(builder_key, create_builder(repo, &block))
57
+ end
58
+
59
+ def self.create_repo(type, domain_name)
60
+ repo_class = "Appfuel::#{type.to_s.classify}::Repository"
61
+ unless Kernel.const_defined?(repo_class)
62
+ fail "Could not find #{repo_class} for entity builder #{domain_name}"
63
+ end
64
+ Kernel.const_get(repo_class).new
65
+ end
66
+
67
+ def self.create_builder(repo, &block)
68
+ ->(storage, criteria) {
69
+ repo.instance_exec(storage, criteria, &block)
70
+ }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,72 @@
1
+ module Appfuel
2
+ module Repository
3
+ class Base
4
+ include Appfuel::Application::AppContainer
5
+
6
+ class << self
7
+ attr_writer :mapper
8
+ def inherited(klass)
9
+ register_container_class(klass)
10
+ end
11
+
12
+ def mapper
13
+ @mapper ||= create_mapper
14
+ end
15
+
16
+ def create_mapper(maps = nil)
17
+ Mapper.new(maps)
18
+ end
19
+ end
20
+
21
+ def mapper
22
+ self.class.mapper
23
+ end
24
+
25
+ def to_storage(entity, exclude = [])
26
+ mapper.to_storage(entity, exclude)
27
+ end
28
+
29
+ def to_entity(domain_name, storage)
30
+ key = qualify_container_key(domain_name, "domains")
31
+ hash = mapper.to_entity_hash(domain_name, storage)
32
+ app_container[key].new(hash)
33
+ end
34
+
35
+ def build(type:, name:, storage:, **inputs)
36
+ builder = find_entity_builder(type: type, domain_name: name)
37
+ builder.call(storage, inputs)
38
+ end
39
+
40
+ # features.membership.presenters.hash.user
41
+ # global.presenters.user
42
+ #
43
+ # key => db_model
44
+ # key => db_model
45
+ def find_entity_builder(domain_name:, type:)
46
+ key = qualify_container_key(domain_name, "domain_builders.#{type}")
47
+
48
+ container = app_container
49
+ unless container.key?(key)
50
+ return ->(data, inputs = {}) {
51
+ build_default_entity(domain_name: domain_name, storage: data)
52
+ }
53
+ end
54
+
55
+ container[key]
56
+ end
57
+
58
+ def build_default_entity(domain_name:, storage:)
59
+ storage = [storage] unless storage.is_a?(Array)
60
+
61
+ storage_attrs = {}
62
+ storage.each do |model|
63
+ storage_attrs.merge!(mapper.model_attributes(model))
64
+ end
65
+
66
+ hash = mapper.to_entity_hash(domain_name, storage_attrs)
67
+ key = qualify_container_key(domain_name, "domains")
68
+ app_container[key].new(hash)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ module Appfuel
2
+ module Repository
3
+ class Initializer
4
+ def call(container)
5
+ config = container[:config]
6
+ root_path = container[:root_path]
7
+ path = config[:repo_mapping_path]
8
+ unless path
9
+ path = "#{root_path}/storage/mappings"
10
+ end
11
+
12
+ unless ::File.exist?(path)
13
+ fail "Failed to load repo maps, file #{path} does not exist"
14
+ end
15
+ require path
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,203 @@
1
+ module Appfuel
2
+ module Repository
3
+ # The mapping registry holds all entity to db mappings. Mappings are
4
+ # contained within a DbEntityMapEntry object and are arranged by
5
+ # entity name. Each entity will hold a hash where the keys are the
6
+ # attribute names and the value is the entry
7
+ class Mapper
8
+ attr_reader :container_root_name
9
+
10
+ def initialize(app_name, map = nil)
11
+ @container_root_name = app_name
12
+ if !map.nil? && !map.is_a?(Hash)
13
+ fail "repository mappings must be a hash"
14
+ end
15
+ @map = map
16
+ end
17
+
18
+ # The map represents domain mappings to one or more storage systems.
19
+ # Currently one map represents all storage. So if you have a file, and
20
+ # database storage for a given domain the storage attributes are the same
21
+ # for each interface. This will load the repository mappings from the
22
+ # application container if no map as been manually set.
23
+ #
24
+ # @example a map has the following structure
25
+ # {
26
+ # domain_name: {
27
+ # domain_attr1: <MappingEntry>,
28
+ # domain_attr1: <MappingEntry>
29
+ # }
30
+ # ...
31
+ # }
32
+ # @return [Hash]
33
+ def map
34
+ @map ||= mappings_from_container
35
+ end
36
+
37
+ # Determine if an entity has been added
38
+ #
39
+ # @param entity [String]
40
+ # @return [Boolean]
41
+ def entity?(entity_name)
42
+ map.key?(entity_name)
43
+ end
44
+
45
+ # Determine if an attribute is mapped for a given entity
46
+ #
47
+ # @param entity [String] name of the entity
48
+ # @param attr [String] name of the attribute
49
+ # @return [Boolean]
50
+ def entity_attr?(entity_name, entity_attr)
51
+ return false unless entity?(entity_name)
52
+
53
+ map[entity_name].key?(entity_attr)
54
+ end
55
+
56
+ # Returns a mapping entry for a given entity
57
+ #
58
+ # @raise [RuntimeError] when entity not found
59
+ # @raise [RuntimeError] when attr not found
60
+ #
61
+ # @param entity_name [String] qualified entity name "<feature>.<entity>"
62
+ # @param entity_attr [String] name of the attribute
63
+ # @return [Boolean]
64
+ def find(entity_name, entity_attr)
65
+ validate_domain(entity_name)
66
+
67
+ unless map[entity_name].key?(entity_attr)
68
+ fail "Entity (#{entity_name}) attr (#{entity_attr}) is not registered"
69
+ end
70
+ map[entity_name][entity_attr]
71
+ end
72
+
73
+ # Iterates over all entries for a given entity
74
+ #
75
+ # @yield [attr, entry] expose the entity attr name and entry
76
+ #
77
+ # @param entity_name [String] qualified entity name "<feature>.<entity>"
78
+ # @return [void]
79
+ def each_entity_attr(entity_name)
80
+ validate_domain(entity_name)
81
+ map[entity_name].each do |_attr, entry|
82
+ yield entry
83
+ end
84
+ end
85
+
86
+ # Determine if an column is mapped for a given entity
87
+ #
88
+ # @param entity_name [String] qualified entity name "<feature>.<entity>"
89
+ # @param storage_attr [String] name the persistence attr
90
+ # @return [Boolean]
91
+ def storage_attr_mapped?(entity_name, storage_attr)
92
+ each_entity_attr(entity_name) do |entry|
93
+ return true if storage_attr == entry.storage_attr
94
+ end
95
+
96
+ false
97
+ end
98
+
99
+ # Returns a column name for an entity's attribute
100
+ #
101
+ # @raise [RuntimeError] when entity not found
102
+ # @raise [RuntimeError] when attr not found
103
+ #
104
+ # @param entity_name [String] qualified entity name "<feature>.<entity>"
105
+ # @param entity_attr [String] name of the attribute
106
+ # @return [String]
107
+ def storage_attr(entity_name, entity_attr)
108
+ find(entity_name, entity_attr).storage_attr
109
+ end
110
+
111
+ # Returns the storage class based on type
112
+ # mapping foo.bar, db: auth.foo_bar,
113
+ #
114
+ # @raise [RuntimeError] when entity not found
115
+ # @raise [RuntimeError] when attr not found
116
+ # @raise [Dry::Contriner::Error] when db_class is not registered
117
+ #
118
+ # @param entity [String] name of the entity
119
+ # @param attr [String] name of the attribute
120
+ # @return [Object]
121
+ def storage_class(domain_name, domain_attr, type)
122
+ entry = find(domain_name, attr)
123
+
124
+ unless entry.storage?(type)
125
+ fail "No (#{type}) storage has been mapped"
126
+ end
127
+
128
+ container_name = entry.container_name
129
+ unless container_root_name == container_name
130
+ fail "You can not access a mapping outside of this container " +
131
+ "(#{container_root_name}, #{container_name})"
132
+ end
133
+ app_container = Appfuel.app_container(entry.container)
134
+ key = entry.storage(type)
135
+ app_container[key]
136
+ end
137
+
138
+ def to_entity_hash(domain_name, data, opts = {})
139
+ entity_attrs = {}
140
+ each_entity_attr(domain_name) do |entry|
141
+ attr_name = entry.storage_attr
142
+ domain_attr = entry.domain_attr
143
+ next unless data.key?(attr_name)
144
+
145
+ update_entity_hash(domain_attr, data[attr_name], entity_attrs)
146
+ end
147
+
148
+ entity_attrs
149
+ end
150
+
151
+ def update_entity_hash(domain_attr, value, hash)
152
+ if domain_attr.include?('.')
153
+ hash.deep_merge!(create_entity_hash(domain_attr, value))
154
+ else
155
+ hash[domain_attr] = value
156
+ end
157
+ end
158
+
159
+ def entity_value(domain, map_entry)
160
+ value = resolve_entity_value(domain, map_entry.domain_attr)
161
+ if map_entry.computed_attr?
162
+ value = map_entry.compute_attr(domain, value)
163
+ end
164
+
165
+ value = nil if undefined?(value)
166
+
167
+ value
168
+ end
169
+
170
+ def create_entity_hash(domain_attr, value)
171
+ domain_attr.split('.').reverse.inject(value) do |result, nested_attr|
172
+ {nested_attr => result}
173
+ end
174
+ end
175
+
176
+ def undefined?(value)
177
+ value == Types::Undefined
178
+ end
179
+
180
+ def resolve_entity_value(domain, entity_attr)
181
+ chain = entity_attr.split('.')
182
+ target = domain
183
+ chain.each do |attr_method|
184
+ return nil unless target.respond_to?(attr_method)
185
+ target = target.public_send(attr_method)
186
+ end
187
+ target
188
+ end
189
+
190
+ private
191
+ def validate_domain(entity_name)
192
+ unless entity?(entity_name)
193
+ fail "Entity (#{entity_name}) is not registered"
194
+ end
195
+ end
196
+
197
+ def mappings_from_container
198
+ container = Appfuel.app_container(container_root_name)
199
+ container[:repository_mappings]
200
+ end
201
+ end
202
+ end
203
+ end