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