appfuel 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +25 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +19 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +9 -0
  9. data/README.md +38 -0
  10. data/Rakefile +6 -0
  11. data/appfuel.gemspec +42 -0
  12. data/bin/console +7 -0
  13. data/bin/setup +8 -0
  14. data/lib/appfuel.rb +210 -0
  15. data/lib/appfuel/application.rb +4 -0
  16. data/lib/appfuel/application/app_container.rb +223 -0
  17. data/lib/appfuel/application/container_class_registration.rb +22 -0
  18. data/lib/appfuel/application/container_key.rb +201 -0
  19. data/lib/appfuel/application/qualify_container_key.rb +76 -0
  20. data/lib/appfuel/application/root.rb +140 -0
  21. data/lib/appfuel/cli_msg_request.rb +19 -0
  22. data/lib/appfuel/configuration.rb +14 -0
  23. data/lib/appfuel/configuration/definition_dsl.rb +175 -0
  24. data/lib/appfuel/configuration/file_loader.rb +61 -0
  25. data/lib/appfuel/configuration/populate.rb +95 -0
  26. data/lib/appfuel/configuration/search.rb +45 -0
  27. data/lib/appfuel/db_model.rb +16 -0
  28. data/lib/appfuel/domain.rb +7 -0
  29. data/lib/appfuel/domain/criteria.rb +436 -0
  30. data/lib/appfuel/domain/domain_name_parser.rb +44 -0
  31. data/lib/appfuel/domain/dsl.rb +247 -0
  32. data/lib/appfuel/domain/entity.rb +242 -0
  33. data/lib/appfuel/domain/entity_collection.rb +87 -0
  34. data/lib/appfuel/domain/expr.rb +127 -0
  35. data/lib/appfuel/domain/value_object.rb +7 -0
  36. data/lib/appfuel/errors.rb +104 -0
  37. data/lib/appfuel/feature.rb +2 -0
  38. data/lib/appfuel/feature/action_loader.rb +25 -0
  39. data/lib/appfuel/feature/initializer.rb +43 -0
  40. data/lib/appfuel/handler.rb +6 -0
  41. data/lib/appfuel/handler/action.rb +17 -0
  42. data/lib/appfuel/handler/base.rb +103 -0
  43. data/lib/appfuel/handler/command.rb +18 -0
  44. data/lib/appfuel/handler/inject_dsl.rb +88 -0
  45. data/lib/appfuel/handler/validator_dsl.rb +256 -0
  46. data/lib/appfuel/initialize.rb +70 -0
  47. data/lib/appfuel/initialize/initializer.rb +68 -0
  48. data/lib/appfuel/msg_request.rb +207 -0
  49. data/lib/appfuel/predicates.rb +10 -0
  50. data/lib/appfuel/presenter.rb +18 -0
  51. data/lib/appfuel/presenter/base.rb +7 -0
  52. data/lib/appfuel/repository.rb +73 -0
  53. data/lib/appfuel/repository/base.rb +72 -0
  54. data/lib/appfuel/repository/initializer.rb +19 -0
  55. data/lib/appfuel/repository/mapper.rb +203 -0
  56. data/lib/appfuel/repository/mapping_dsl.rb +210 -0
  57. data/lib/appfuel/repository/mapping_entry.rb +76 -0
  58. data/lib/appfuel/repository/mapping_registry.rb +121 -0
  59. data/lib/appfuel/repository_runner.rb +60 -0
  60. data/lib/appfuel/request.rb +53 -0
  61. data/lib/appfuel/response.rb +96 -0
  62. data/lib/appfuel/response_handler.rb +79 -0
  63. data/lib/appfuel/root_module.rb +31 -0
  64. data/lib/appfuel/run_error.rb +9 -0
  65. data/lib/appfuel/storage.rb +3 -0
  66. data/lib/appfuel/storage/db.rb +4 -0
  67. data/lib/appfuel/storage/db/active_record_model.rb +42 -0
  68. data/lib/appfuel/storage/db/mapper.rb +213 -0
  69. data/lib/appfuel/storage/db/migration_initializer.rb +42 -0
  70. data/lib/appfuel/storage/db/migration_runner.rb +15 -0
  71. data/lib/appfuel/storage/db/migration_tasks.rb +18 -0
  72. data/lib/appfuel/storage/db/repository.rb +231 -0
  73. data/lib/appfuel/storage/db/repository_query.rb +13 -0
  74. data/lib/appfuel/storage/file.rb +1 -0
  75. data/lib/appfuel/storage/file/base.rb +32 -0
  76. data/lib/appfuel/storage/memory.rb +2 -0
  77. data/lib/appfuel/storage/memory/mapper.rb +30 -0
  78. data/lib/appfuel/storage/memory/repository.rb +37 -0
  79. data/lib/appfuel/types.rb +53 -0
  80. data/lib/appfuel/validation.rb +80 -0
  81. data/lib/appfuel/validation/validator.rb +59 -0
  82. data/lib/appfuel/validation/validator_pipe.rb +47 -0
  83. data/lib/appfuel/version.rb +3 -0
  84. metadata +335 -0
@@ -0,0 +1,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