appfuel 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|