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