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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +25 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +19 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/README.md +38 -0
- data/Rakefile +6 -0
- data/appfuel.gemspec +42 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/appfuel.rb +210 -0
- data/lib/appfuel/application.rb +4 -0
- data/lib/appfuel/application/app_container.rb +223 -0
- data/lib/appfuel/application/container_class_registration.rb +22 -0
- data/lib/appfuel/application/container_key.rb +201 -0
- data/lib/appfuel/application/qualify_container_key.rb +76 -0
- data/lib/appfuel/application/root.rb +140 -0
- data/lib/appfuel/cli_msg_request.rb +19 -0
- data/lib/appfuel/configuration.rb +14 -0
- data/lib/appfuel/configuration/definition_dsl.rb +175 -0
- data/lib/appfuel/configuration/file_loader.rb +61 -0
- data/lib/appfuel/configuration/populate.rb +95 -0
- data/lib/appfuel/configuration/search.rb +45 -0
- data/lib/appfuel/db_model.rb +16 -0
- data/lib/appfuel/domain.rb +7 -0
- data/lib/appfuel/domain/criteria.rb +436 -0
- data/lib/appfuel/domain/domain_name_parser.rb +44 -0
- data/lib/appfuel/domain/dsl.rb +247 -0
- data/lib/appfuel/domain/entity.rb +242 -0
- data/lib/appfuel/domain/entity_collection.rb +87 -0
- data/lib/appfuel/domain/expr.rb +127 -0
- data/lib/appfuel/domain/value_object.rb +7 -0
- data/lib/appfuel/errors.rb +104 -0
- data/lib/appfuel/feature.rb +2 -0
- data/lib/appfuel/feature/action_loader.rb +25 -0
- data/lib/appfuel/feature/initializer.rb +43 -0
- data/lib/appfuel/handler.rb +6 -0
- data/lib/appfuel/handler/action.rb +17 -0
- data/lib/appfuel/handler/base.rb +103 -0
- data/lib/appfuel/handler/command.rb +18 -0
- data/lib/appfuel/handler/inject_dsl.rb +88 -0
- data/lib/appfuel/handler/validator_dsl.rb +256 -0
- data/lib/appfuel/initialize.rb +70 -0
- data/lib/appfuel/initialize/initializer.rb +68 -0
- data/lib/appfuel/msg_request.rb +207 -0
- data/lib/appfuel/predicates.rb +10 -0
- data/lib/appfuel/presenter.rb +18 -0
- data/lib/appfuel/presenter/base.rb +7 -0
- data/lib/appfuel/repository.rb +73 -0
- data/lib/appfuel/repository/base.rb +72 -0
- data/lib/appfuel/repository/initializer.rb +19 -0
- data/lib/appfuel/repository/mapper.rb +203 -0
- data/lib/appfuel/repository/mapping_dsl.rb +210 -0
- data/lib/appfuel/repository/mapping_entry.rb +76 -0
- data/lib/appfuel/repository/mapping_registry.rb +121 -0
- data/lib/appfuel/repository_runner.rb +60 -0
- data/lib/appfuel/request.rb +53 -0
- data/lib/appfuel/response.rb +96 -0
- data/lib/appfuel/response_handler.rb +79 -0
- data/lib/appfuel/root_module.rb +31 -0
- data/lib/appfuel/run_error.rb +9 -0
- data/lib/appfuel/storage.rb +3 -0
- data/lib/appfuel/storage/db.rb +4 -0
- data/lib/appfuel/storage/db/active_record_model.rb +42 -0
- data/lib/appfuel/storage/db/mapper.rb +213 -0
- data/lib/appfuel/storage/db/migration_initializer.rb +42 -0
- data/lib/appfuel/storage/db/migration_runner.rb +15 -0
- data/lib/appfuel/storage/db/migration_tasks.rb +18 -0
- data/lib/appfuel/storage/db/repository.rb +231 -0
- data/lib/appfuel/storage/db/repository_query.rb +13 -0
- data/lib/appfuel/storage/file.rb +1 -0
- data/lib/appfuel/storage/file/base.rb +32 -0
- data/lib/appfuel/storage/memory.rb +2 -0
- data/lib/appfuel/storage/memory/mapper.rb +30 -0
- data/lib/appfuel/storage/memory/repository.rb +37 -0
- data/lib/appfuel/types.rb +53 -0
- data/lib/appfuel/validation.rb +80 -0
- data/lib/appfuel/validation/validator.rb +59 -0
- data/lib/appfuel/validation/validator_pipe.rb +47 -0
- data/lib/appfuel/version.rb +3 -0
- 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,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,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
|