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,210 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Repository
|
3
|
+
# A mapping dsl that allows the collection of database columns to domain
|
4
|
+
# attributes. The reason this dsl is separated from the DbEntityMapper
|
5
|
+
# is due to the fact that we use method_missing for collecting column names
|
6
|
+
# and don't want any incorrect method_missing calls to be confused when
|
7
|
+
# collecting mapped values vs when defining them.
|
8
|
+
class MappingDsl
|
9
|
+
attr_reader :domain_name, :storage, :entries, :entry_class,
|
10
|
+
:container_name
|
11
|
+
|
12
|
+
STORAGE_TYPES = [:db, :file, :memory]
|
13
|
+
|
14
|
+
# 1) mapping 'feature.domain', db: true, do
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# 2) mapping 'feature.domain', db: 'foo.bar', do
|
19
|
+
# ...
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# 3) mapping 'feature.domain', db: 'global.bar' do
|
23
|
+
# ...
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# 4) mapping 'feature.domain', db: 'foo.bar.baz', key_translation: false)
|
27
|
+
# 4) mapping 'feature.domain', storage: [:db, :file] do
|
28
|
+
# ...
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# a file model requires the domain_name it represents.
|
32
|
+
#
|
33
|
+
# case1 - build a model with default settings
|
34
|
+
# file: storage.file.model
|
35
|
+
# path: <root_path>/storage/file/{key}.yaml
|
36
|
+
# adapter: storage.file.model
|
37
|
+
#
|
38
|
+
# case2 - build a model with given settings
|
39
|
+
# note: if path is absolute nothing is done
|
40
|
+
# if path is relative we will prepend root_path
|
41
|
+
# if no yaml extension the key is translated to a path
|
42
|
+
#
|
43
|
+
# path: foo/bar/baz.yml -> <root_path>/foo/bar/baz.yml
|
44
|
+
# path: /foo/bar/baz.yml -> /foo/bar/baz.yml
|
45
|
+
# path auth.user -> <root_path>/storage/features/auth/file/user.yml
|
46
|
+
# path gobal.user -> <root_path/storage/global/auth/file/user.yml
|
47
|
+
#
|
48
|
+
# case3 - build a model with adapter and path
|
49
|
+
# path: sames as above
|
50
|
+
# adapter translates key to location of adapter in storage
|
51
|
+
#
|
52
|
+
# container
|
53
|
+
# default key -> storage.file.model is default
|
54
|
+
# auth.user -> features.auth.storage.file.user
|
55
|
+
#
|
56
|
+
# file 'storage.file.model'
|
57
|
+
#
|
58
|
+
# storage db: 'foo.user_user'
|
59
|
+
# storage :file
|
60
|
+
# storage :memory
|
61
|
+
#
|
62
|
+
# storage db: 'foo.user_ath',
|
63
|
+
# file: 'storage.file.model',
|
64
|
+
# memory: 'storage.memory.model'
|
65
|
+
#
|
66
|
+
def initialize(domain_name, options = {})
|
67
|
+
fail "options must be a hash" unless options.is_a?(Hash)
|
68
|
+
|
69
|
+
@entries = []
|
70
|
+
@domain_name = domain_name.to_s
|
71
|
+
@entry_class = options[:entry_class] || MappingEntry
|
72
|
+
@container_name = options[:container] || Appfuel.default_app_name
|
73
|
+
@storage = initialize_storage(options)
|
74
|
+
|
75
|
+
fail "entity name can not be empty" if @domain_name.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
def db(key)
|
79
|
+
@storage[:db] = translate_storage_key(key)
|
80
|
+
end
|
81
|
+
|
82
|
+
# 5) mapping 'feature.domain' db: true do
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# 6) mapping 'feature.domain', db: true do
|
86
|
+
# storage :db, 'foo.bar'
|
87
|
+
# storage :file
|
88
|
+
# end
|
89
|
+
# storage(type = nil, options = {})
|
90
|
+
#
|
91
|
+
def storage(type = nil, *args)
|
92
|
+
return @storage if type.nil?
|
93
|
+
unless type.respond_to?(:to_sym)
|
94
|
+
fail "Storage type must implement :to_sym"
|
95
|
+
end
|
96
|
+
type = type.to_sym
|
97
|
+
|
98
|
+
if all_storage_symbols?(*args)
|
99
|
+
args.unshift(type)
|
100
|
+
args.each do |storage_type|
|
101
|
+
@storage[storage_type] = send("initialize_#{storage_type}_storage", true)
|
102
|
+
end
|
103
|
+
|
104
|
+
return self
|
105
|
+
end
|
106
|
+
|
107
|
+
args = [true] if args.empty?
|
108
|
+
|
109
|
+
key = args.shift
|
110
|
+
opts = args.shift
|
111
|
+
data = {type => key}
|
112
|
+
if opts.is_a?(Hash)
|
113
|
+
data.merge!(opts)
|
114
|
+
end
|
115
|
+
|
116
|
+
@storage.merge!(initialize_storage(data))
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def all_storage_symbols?(*args)
|
121
|
+
result = args - STORAGE_TYPES
|
122
|
+
result.empty?
|
123
|
+
end
|
124
|
+
|
125
|
+
def map(name, domain_attr = nil, opts = {})
|
126
|
+
domain_attr = name if domain_attr.nil?
|
127
|
+
|
128
|
+
data = opts.merge({
|
129
|
+
domain_name: domain_name,
|
130
|
+
domain_attr: domain_attr,
|
131
|
+
storage: storage,
|
132
|
+
storage_attr: name,
|
133
|
+
container: container_name,
|
134
|
+
})
|
135
|
+
|
136
|
+
@entries << entry_class.new(data)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def initialize_storage(data)
|
142
|
+
storage = {}
|
143
|
+
if data.key?(:db)
|
144
|
+
value = data[:db]
|
145
|
+
storage[:db] = initialize_db_storage(value, data)
|
146
|
+
elsif data.key?(:file)
|
147
|
+
value = data[:file]
|
148
|
+
storage[:file] = initialize_default_storage(value, :file)
|
149
|
+
elsif data.key?(:storage) && data[:storage].is_a?(Array)
|
150
|
+
data[:storage].each do |type|
|
151
|
+
storage[type] = send("initialize_#{type}_storage", true)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
storage
|
155
|
+
end
|
156
|
+
|
157
|
+
def initialize_db_storage(value, opts = {})
|
158
|
+
case
|
159
|
+
when value == true
|
160
|
+
translate_storage_key(:db, domain_name)
|
161
|
+
when opts.is_a?(Hash) && opts[:key_translation] == false
|
162
|
+
value
|
163
|
+
else
|
164
|
+
translate_storage_key(:db, value)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def initialize_file_storage(value, opts = {})
|
169
|
+
key = translate_storage_key(:file, domain_name)
|
170
|
+
case value
|
171
|
+
when true
|
172
|
+
{
|
173
|
+
model: 'storage.file.model',
|
174
|
+
path: "#{storage_path}/#{key.gsub(/\./,'/')}.yml"
|
175
|
+
}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def storage_path
|
180
|
+
app_container = Appfuel.app_container(container_name)
|
181
|
+
path = app_container[:root_path]
|
182
|
+
if app_container.key?(:storage_path)
|
183
|
+
path = app_container[:storage_path]
|
184
|
+
end
|
185
|
+
path
|
186
|
+
end
|
187
|
+
|
188
|
+
#
|
189
|
+
# global.user
|
190
|
+
# global.storage.db.user
|
191
|
+
# membership.user
|
192
|
+
# features.membership.{type}.user
|
193
|
+
def translate_storage_key(type, partial_key)
|
194
|
+
fail "#{type} can not be empty" if partial_key.empty?
|
195
|
+
|
196
|
+
top, *parts = partial_key.split('.')
|
197
|
+
top = "features.#{top}" unless top == 'global'
|
198
|
+
"#{top}.storage.#{type}.#{parts.join('.')}"
|
199
|
+
end
|
200
|
+
|
201
|
+
def assign_storage(type, partial_key)
|
202
|
+
@storage[type] = translate_storage_key(partial_key)
|
203
|
+
end
|
204
|
+
|
205
|
+
def assign_default_storage(type)
|
206
|
+
@storage[type] = "#{type.to_s.underscore}.model"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Repository
|
3
|
+
class MappingEntry
|
4
|
+
attr_reader :domain_name, :domain_attr, :computed_attr, :storage_attr,
|
5
|
+
:container_name, :container_key
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
unless data.respond_to?(:to_h)
|
9
|
+
fail "Map entry data must respond to :to_h"
|
10
|
+
end
|
11
|
+
|
12
|
+
data = data.to_h
|
13
|
+
@domain_name = data.fetch(:domain_name) {
|
14
|
+
fail "Fully qualified domain name is required"
|
15
|
+
}.to_s
|
16
|
+
|
17
|
+
@storage = data.fetch(:storage) {
|
18
|
+
fail "Storage classes hash is required"
|
19
|
+
}
|
20
|
+
|
21
|
+
@storage_attr = data.fetch(:storage_attr) {
|
22
|
+
fail "Storage attribute is required"
|
23
|
+
}.to_s
|
24
|
+
|
25
|
+
@domain_attr = data.fetch(:domain_attr) {
|
26
|
+
fail "Domain attribute is required"
|
27
|
+
}
|
28
|
+
|
29
|
+
@skip = data[:skip] == true ? true : false
|
30
|
+
|
31
|
+
if data.key?(:computed_attr)
|
32
|
+
computed_attr_lambda(data[:computed_attr])
|
33
|
+
end
|
34
|
+
|
35
|
+
@container_name = data[:container]
|
36
|
+
@container_key = "mappings.#{domain_name}.#{domain_attr}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def storage(type)
|
40
|
+
fail "Storage #{type} is not registered" unless storage?(type)
|
41
|
+
|
42
|
+
@storage[type]
|
43
|
+
end
|
44
|
+
|
45
|
+
def storage?(type)
|
46
|
+
@storage.key?(type)
|
47
|
+
end
|
48
|
+
|
49
|
+
def skip?
|
50
|
+
@skip
|
51
|
+
end
|
52
|
+
|
53
|
+
def computed_attr?
|
54
|
+
!computed_attr.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
def compute_attr(value, domain)
|
58
|
+
fail "No lambda assigned to compute value" unless computed_attr?
|
59
|
+
@computed_attr.call(value, domain)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def computed_attr_lambda(value)
|
64
|
+
unless value.lambda?
|
65
|
+
fail "computed attributes require a lambda as a value"
|
66
|
+
end
|
67
|
+
|
68
|
+
if value.arity != 2
|
69
|
+
fail "computed attribute lambda's must accept 2 param"
|
70
|
+
end
|
71
|
+
|
72
|
+
@computed_attr = value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,121 @@
|
|
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 MappingRegistry
|
8
|
+
attr_reader :map, :container_root_name
|
9
|
+
|
10
|
+
def initialize(app_name, map)
|
11
|
+
@container_root_name = app_name
|
12
|
+
@map = map
|
13
|
+
end
|
14
|
+
|
15
|
+
# Determine if an entity has been added
|
16
|
+
#
|
17
|
+
# @param entity [String]
|
18
|
+
# @return [Boolean]
|
19
|
+
def domain?(domain_name)
|
20
|
+
map.key?(domain_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Determine if an attribute is mapped for a given entity
|
24
|
+
#
|
25
|
+
# @param entity [String] name of the entity
|
26
|
+
# @param attr [String] name of the attribute
|
27
|
+
# @return [Boolean]
|
28
|
+
def domain_attr?(domain_name, domain_attr)
|
29
|
+
return false unless entity?(domain_name)
|
30
|
+
map[domain_name].key?(domain_attr)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns a mapping entry for a given entity
|
34
|
+
#
|
35
|
+
# @raise [RuntimeError] when entity not found
|
36
|
+
# @raise [RuntimeError] when attr not found
|
37
|
+
#
|
38
|
+
# @param entity [String] name of the entity
|
39
|
+
# @param attr [String] name of the attribute
|
40
|
+
# @return [Boolean]
|
41
|
+
def find(domain_name, domain_attr)
|
42
|
+
validate_entity(domain_name)
|
43
|
+
|
44
|
+
unless map[domain_name].key?(domain_attr)
|
45
|
+
fail "Entity (#{domain_name}) attr (#{domain_attr}) not registered"
|
46
|
+
end
|
47
|
+
map[domain_name][domain_attr]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterates over all entries for a given entity
|
51
|
+
#
|
52
|
+
# @yield [attr, entry] expose the entity attr name and entry
|
53
|
+
#
|
54
|
+
# @param entity [String] name of the entity
|
55
|
+
# @return [void]
|
56
|
+
def each_domain_attr(domain_name)
|
57
|
+
validate_domain(domain_name)
|
58
|
+
|
59
|
+
map[domain_name].each do |attr, entry|
|
60
|
+
yield attr, entry
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Determine if an column is mapped for a given entity
|
65
|
+
#
|
66
|
+
# @param entity [String] name of the entity
|
67
|
+
# @param attr [String] name of the attribute
|
68
|
+
# @return [Boolean]
|
69
|
+
def persistence_attr_mapped?(domain_name, persistence_attr)
|
70
|
+
result = false
|
71
|
+
each_domain_attr(entity) do |_attr, entry|
|
72
|
+
result = true if persistence_attr == entry.persistence_attr
|
73
|
+
end
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns a column name for an entity's attribute
|
78
|
+
#
|
79
|
+
# @raise [RuntimeError] when entity not found
|
80
|
+
# @raise [RuntimeError] when attr not found
|
81
|
+
#
|
82
|
+
# @param entity [String] name of the entity
|
83
|
+
# @param attr [String] name of the attribute
|
84
|
+
# @return [String]
|
85
|
+
def persistence_attr(domain_name, attr)
|
86
|
+
find(entity, attr).persistence_attr
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the db model for a given entity attr
|
90
|
+
# container:
|
91
|
+
# domains:
|
92
|
+
# domain_name -> domain
|
93
|
+
# persistence
|
94
|
+
# db:
|
95
|
+
# persistence_name: -> class
|
96
|
+
#
|
97
|
+
# container[persistence.db.name]
|
98
|
+
#
|
99
|
+
# @raise [RuntimeError] when entity not found
|
100
|
+
# @raise [RuntimeError] when attr not found
|
101
|
+
# @raise [Dry::Contriner::Error] when db_class is not registered
|
102
|
+
#
|
103
|
+
# @param entity [String] name of the entity
|
104
|
+
# @param attr [String] name of the attribute
|
105
|
+
# @return [Object]
|
106
|
+
def persistence_class(type, domain_name, attr)
|
107
|
+
entry = find(domain_name, attr)
|
108
|
+
name = entry.persistence[type]
|
109
|
+
key = "persistence.#{type}.#{name}"
|
110
|
+
Appfuel.app_container(root_name)[key]
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def validate_entity(entity)
|
115
|
+
unless entity?(entity)
|
116
|
+
fail "Entity (#{entity}) is not registered"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Appfuel
|
2
|
+
# Used in the validation system by custom predicates to ask the question if
|
3
|
+
# an entity exists in the database
|
4
|
+
class RepositoryRunner
|
5
|
+
attr_reader :repo_namespace, :criteria_class
|
6
|
+
|
7
|
+
# The call relies on the fact that we can build a criteria find the
|
8
|
+
# correct repo and call the exists? interface on that repo. The identity
|
9
|
+
# of any given repo requires its namespace + its class name.
|
10
|
+
#
|
11
|
+
# @param namespace [String] fully qualified namespace string fro repos
|
12
|
+
# @param criteria_class [Class] class used to represent the criteria
|
13
|
+
# @returns [ExistsInDbRunner]
|
14
|
+
def initialize(namespace, criteria_class)
|
15
|
+
@repo_namespace = namespace
|
16
|
+
@criteria_class = criteria_class
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_criteria(entity_key, opts = {})
|
20
|
+
criteria_class.new(entity_key, opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
def query(criteria)
|
24
|
+
load_repo(criteria).query(criteria)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param entity_key [String] the type identifier for an entity
|
28
|
+
# @param opts [Hash] one attr => value pair is required
|
29
|
+
# repo => name is optional
|
30
|
+
#
|
31
|
+
# @return [Bool]
|
32
|
+
def exists?(entity_key, opts = {})
|
33
|
+
fail "opts must be a hash" unless opts.is_a?(Hash)
|
34
|
+
|
35
|
+
criteria_opts = {}
|
36
|
+
if opts.key?(:repo)
|
37
|
+
criteria_opts[:repo] = opts.delete(:repo)
|
38
|
+
end
|
39
|
+
fail "opts hash must have one attr => value pair" if opts.empty?
|
40
|
+
|
41
|
+
property, value = opts.first
|
42
|
+
criteria = create_criteria(entity_key, criteria_opts)
|
43
|
+
criteria.exists(property, value)
|
44
|
+
|
45
|
+
load_repo(criteria).exists?(criteria)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def load_repo(criteria)
|
52
|
+
klass = "#{repo_namespace}::#{criteria.repo_name}"
|
53
|
+
unless Kernel.const_defined?(klass)
|
54
|
+
fail "RepositoryRunner: failed - repo #{klass} not defined"
|
55
|
+
end
|
56
|
+
|
57
|
+
Kernel.const_get(klass).new
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|