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,87 @@
1
+ module Appfuel
2
+ module Domain
3
+ # Currently this only answers the use case where a collection of active
4
+ # record models are converted into a collection of domain entities via
5
+ # a entity loader.
6
+ #
7
+ # NOTE: There is no ability yet to add or track the entity state
8
+ #
9
+ class EntityCollection
10
+ include Enumerable
11
+ attr_reader :domain_name, :domain_basename, :entity_loader
12
+
13
+ def initialize(domain_name, entity_loader = nil)
14
+ unless Types.key?(domain_name)
15
+ fail "#{domain_name} is not a registered type"
16
+ end
17
+
18
+ @pager = nil
19
+ @list = []
20
+ @loaded = false
21
+
22
+ parts = domain_name.split('.')
23
+ @domain_name = domain_name
24
+ @domain_basename = parts.last
25
+ @is_global = parts.size == 1
26
+
27
+ self.entity_loader = entity_loader if entity_loader
28
+ end
29
+
30
+ def collection?
31
+ true
32
+ end
33
+
34
+ def global?
35
+ @is_global
36
+ end
37
+
38
+ def all
39
+ load_entities
40
+ @list
41
+ end
42
+
43
+ def first
44
+ load_entities
45
+ @list.first
46
+ end
47
+
48
+ def each
49
+ load_entities
50
+ return @list.each unless block_given?
51
+
52
+ @list.each {|entity| yield entity}
53
+ end
54
+
55
+ def pager
56
+ load_entities
57
+ @pager
58
+ end
59
+
60
+ def to_a
61
+ list = []
62
+ each do |entity|
63
+ list << entity.to_h
64
+ end
65
+ list
66
+ end
67
+
68
+ def entity_loader?
69
+ !@entity_loader.nil?
70
+ end
71
+
72
+ def entity_loader=(loader)
73
+ fail "Entity loader must implement call" unless loader.respond_to?(:call)
74
+ @entity_loader = loader
75
+ end
76
+
77
+ protected
78
+
79
+ def load_entities
80
+ return false if @loaded || !entity_loader?
81
+ data = entity_loader.call
82
+ @list = data[:list]
83
+ @pager = data[:pager]
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,127 @@
1
+ module Appfuel
2
+ module Domain
3
+ # Domain expressions are used mostly by the criteria to describe filter
4
+ # conditions. The class represents a basic expression like "id eq 6", the
5
+ # problem with this expression is that we need additional information in
6
+ # order to properly map it to something like a db expression. This call
7
+ # ensures that additional information exists. Most importantly we need
8
+ # a fully qualified domain name in the form of "feature.domain".
9
+ class Expr
10
+ include DomainNameParser
11
+ OPS = {
12
+ eq: '=',
13
+ gt: '>',
14
+ gteq: '>=',
15
+ lt: '<',
16
+ lteq: '<=',
17
+ in: 'IN',
18
+ like: 'LIKE',
19
+ ilike: 'ILIKE',
20
+ between: 'BETWEEN'
21
+ }
22
+ attr_reader :feature, :domain_basename, :domain_name, :domain_attr, :value
23
+
24
+ # Assign the fully qualified domain name, its basename and its attribute
25
+ # along with the operator and value. Operator and value are assumed to
26
+ # be the first key => value pair of the hash.
27
+ #
28
+ # @example
29
+ # feature domain
30
+ # Expr.new('foo.bar', 'id', eq: 6)
31
+ #
32
+ # or
33
+ # global domain
34
+ # Expr.new('bar', 'name', like: '%Bob%')
35
+ #
36
+ #
37
+ # @param domain [String] fully qualified domain name
38
+ # @param domain_attr [String, Symbol] attribute name
39
+ # @param data [Hash] holds operator and value
40
+ # @option data [Symbol] the key is the operator and value is the value
41
+ #
42
+ # @return [Expr]
43
+ def initialize(domain, domain_attr, data)
44
+ fail "operator value pair must exist in a hash" unless data.is_a?(Hash)
45
+ @feature, @domain_basename, @domain_name = parse_domain_name(domain)
46
+
47
+ operator, value = data.first
48
+ @domain_attr = domain_attr.to_s
49
+ self.op = operator
50
+ self.value = value
51
+
52
+ fail "domain name can not be empty" if @domain_name.empty?
53
+ fail "domain attribute can not be empty" if @domain_attr.empty?
54
+ end
55
+
56
+ def feature?
57
+ !@feature.nil?
58
+ end
59
+
60
+ def global?
61
+ !feature?
62
+ end
63
+
64
+ # @return [Bool]
65
+ def negated?
66
+ @negated
67
+ end
68
+
69
+ def expr_string
70
+ data = yield domain_name, domain_attr, OPS[op]
71
+ lvalue = data[0]
72
+ operator = data[1]
73
+ rvalue = data[2]
74
+
75
+ operator = "NOT #{operator}" if negated?
76
+ "#{lvalue} #{operator} #{rvalue}"
77
+ end
78
+
79
+ def to_s
80
+ "#{domain_name}.#{domain_attr} #{OPS[op]} #{value}"
81
+ end
82
+
83
+ def op
84
+ negated? ? "not_#{@op}".to_sym : @op
85
+ end
86
+
87
+ private
88
+
89
+ def op=(value)
90
+ negated, value = value.to_s.split('_')
91
+ @negated = false
92
+ if negated == 'not'
93
+ @negated = true
94
+ else
95
+ value = negated
96
+ end
97
+ value = value.to_sym
98
+ unless supported_op?(value)
99
+ fail "op has to be one of [#{OPS.keys.join(',')}]"
100
+ end
101
+ @op = value
102
+ end
103
+
104
+ def value=(data)
105
+ case op
106
+ when :in
107
+ unless data.is_a?(Array)
108
+ fail ":in operator must have an array as a value"
109
+ end
110
+ when :range
111
+ unless data.is_a?(Range)
112
+ fail ":range operator must have a range as a value"
113
+ end
114
+ when :gt, :gteq, :lt, :lteq
115
+ unless data.is_a?(Numeric)
116
+ fail ":gt, :gteq, :lt, :lteq operators expect a numeric value"
117
+ end
118
+ end
119
+ @value = data
120
+ end
121
+
122
+ def supported_op?(op)
123
+ OPS.keys.include?(op)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,7 @@
1
+ module Appfuel
2
+ module Domain
3
+ class ValueObject < Entity
4
+ enable_value_object
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,104 @@
1
+ module Appfuel
2
+ # Feature handler, action handler, command handler all use this class.
3
+ # Presenters and validators will have there errors tranformed into this.
4
+ # Errors are a basic hash structure where each key has an array of strings
5
+ # that represent error messages.
6
+ #
7
+ # Example
8
+ # messages: {
9
+ # name: [
10
+ # 'must be present',
11
+ # 'can not be blank',
12
+ # 'can not be Bob'
13
+ # ]
14
+ # }
15
+ class Errors
16
+ include Enumerable
17
+ attr_reader :messages
18
+
19
+ def initialize(messages = {})
20
+ @messages = messages || {}
21
+ @messages.stringify_keys! unless @messages.empty?
22
+ end
23
+
24
+ # Defined to use Enumerable so that we can treat errors
25
+ # as an iterator
26
+ def each
27
+ messages.each do|key, msgs|
28
+ yield key, msgs
29
+ end
30
+ end
31
+
32
+ # Add an error message to a given key
33
+ #
34
+ # @param key Symbol key for this message
35
+ # @param msg String the message to be stored
36
+ def add(key, msg)
37
+ key = key.to_s
38
+ msg = msg.to_s
39
+ messages[key] = [] unless messages.key?(key)
40
+ messages[key] << msg unless messages[key].include?(msg)
41
+ end
42
+
43
+ # Formats the list of messages for each key
44
+ #
45
+ # Example
46
+ # messages: {
47
+ # name: [
48
+ # ' must be present ',
49
+ # ' can not be blank ',
50
+ # ' can not be Bob '
51
+ # ]
52
+ # }
53
+ #
54
+ # note: spaces are used only for readability
55
+ # name: must be present \n can not be blank \n can not be Bob \n \n
56
+ #
57
+ # @param msg_separator String separates each message default \n
58
+ # @param list_separator String separates each list of messages
59
+ # @return String
60
+ def format(msg_separator = "\n", list_separator = "\n")
61
+ msg = ''
62
+ each do |key, list|
63
+ msg << "#{key}: #{list.join(msg_separator)}#{list_separator}"
64
+ end
65
+ msg
66
+ end
67
+
68
+ def delete(key)
69
+ messages.delete(key.to_s)
70
+ end
71
+
72
+ def [](key)
73
+ messages[key.to_s]
74
+ end
75
+
76
+ def size
77
+ messages.length
78
+ end
79
+
80
+ def values
81
+ messages.values
82
+ end
83
+
84
+ def keys
85
+ messages.keys
86
+ end
87
+
88
+ def clear
89
+ messages.clear
90
+ end
91
+
92
+ def empty?
93
+ messages.empty?
94
+ end
95
+
96
+ def to_h
97
+ {errors: messages}
98
+ end
99
+
100
+ def to_s
101
+ format
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'feature/action_loader'
2
+ require_relative 'feature/initializer'
@@ -0,0 +1,25 @@
1
+ module Appfuel
2
+ module Feature
3
+ # Loads an action from the container using its fully qualified namespace.
4
+ # This class has been abstracted out because its Appfuel's implementation
5
+ # of loading an action. This action loader is injected into the container
6
+ # during setup which allows the client to use their own if this basic
7
+ # lookup mehtod does not work for them.
8
+ #
9
+ # The idea is that all actions, commands and repositories auto register
10
+ # themselves into the container based on a namespace derived inpart by
11
+ # their own ruby namespace.
12
+ class ActionLoader
13
+ # @raises RuntimeError when key is not found
14
+ # @param namespace [String] fully qualifed container namespace
15
+ # @param container [Dry::Container] application container
16
+ # @return [Appfuel::Handler::Action]
17
+ def call(namespace, container)
18
+ unless container.key?(namespace)
19
+ fail "[ActionLoader] Could not load action at #{namespace}"
20
+ end
21
+ container[namespace]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ module Appfuel
2
+ module Feature
3
+ # Run a given feature's initializers. Each feature can declare any number
4
+ # of initializers just as the application does. This allow dependencies
5
+ # and vendor code to be initialized only when the feature is used.
6
+ class Initializer
7
+ # Ensure the correct namespaces are registered so that the initializer
8
+ # dsl will work and then require the feature and run its intializers
9
+ # unless instructed not too. Initializers are only run once.
10
+ #
11
+ # @param name [String] name of the feature as found in the container
12
+ # @param container [Dry::Container] application container
13
+ # @return [Boolean]
14
+ def call(name, container)
15
+ name = name.to_s.underscore
16
+ feature_key = "features.#{name}"
17
+ unless container.key?(feature_key)
18
+ Appfuel.setup_container_dependencies(feature_key, container)
19
+ end
20
+
21
+ unless require_feature_disabled?(container, feature_key)
22
+ require "#{container[:features_path]}/#{name}"
23
+ end
24
+
25
+ return false if initialized?(container, feature_key)
26
+
27
+ Appfuel.run_initializers(feature_key, container)
28
+ true
29
+ end
30
+
31
+ private
32
+ def require_feature_disabled?(container, feature_key)
33
+ disable_key = "#{feature_key}.disable_require"
34
+ container.key?(disable_key) && container[disable_key] == true
35
+ end
36
+
37
+ def initialized?(container, feature_key)
38
+ init_key = "#{feature_key}.initialized"
39
+ container.key?(init_key) && container[init_key] == true
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'handler/validator_dsl'
2
+ require_relative 'handler/inject_dsl'
3
+
4
+ require_relative 'handler/base'
5
+ require_relative 'handler/action'
6
+ require_relative 'handler/command'
@@ -0,0 +1,17 @@
1
+ module Appfuel
2
+ module Handler
3
+ class Action < Base
4
+ class << self
5
+
6
+ # In order to reduce the length of namespaces actions are not required
7
+ # to be inside an Actions namespace, but, it is namespaced with in the
8
+ # application container, so we adjust for that here.
9
+ #
10
+ # @return [String]
11
+ def container_relative_key
12
+ "actions.#{super}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,103 @@
1
+ module Appfuel
2
+ module Handler
3
+ class Base
4
+ extend ValidatorDsl
5
+ extend InjectDsl
6
+ include Appfuel::Application::AppContainer
7
+
8
+ # Class level interfaces used by the framwork to register and run
9
+ # handlers
10
+ class << self
11
+
12
+ # Register the extending class with the application container
13
+ #
14
+ # @param klass [Class] the handler class that is inheriting this
15
+ # @return nil
16
+ def inherited(klass)
17
+ register_container_class(klass)
18
+ end
19
+
20
+ def response_handler
21
+ @response_handler ||= ResponseHandler.new
22
+ end
23
+
24
+ # Run will validate all inputs; returning on input failures, resolving
25
+ # declared dependencies, then delegate to the handlers call method with
26
+ # its valid inputs and resolved dependencies. Finally it ensure every
27
+ # response is a Response object.
28
+ #
29
+ # @param inputs [Hash] inputs to be validated
30
+ # @return [Response]
31
+ def run(inputs = {}, container = Dry::Container.new)
32
+ begin
33
+ response = resolve_inputs(inputs)
34
+ return response if response.failure?
35
+ valid_inputs = response.ok
36
+
37
+ resolve_dependencies(container)
38
+ handler = self.new(container)
39
+ result = handler.call(valid_inputs)
40
+ result = create_response(result) unless response?(result)
41
+ rescue RunError => e
42
+ result = e.response
43
+ rescue StandardError => e
44
+ result = error(e)
45
+ end
46
+
47
+ result
48
+ end
49
+
50
+ def error(*args)
51
+ response_handler.error(*args)
52
+ end
53
+
54
+ def ok(value = nil)
55
+ response_handler.ok(value)
56
+ end
57
+
58
+ def response?(value)
59
+ response_handler.response?(value)
60
+ end
61
+
62
+ def create_response(data)
63
+ response_handler.create_response(data)
64
+ end
65
+
66
+ end
67
+
68
+ attr_reader :data
69
+
70
+ def initialize(container = Dry::Container.new)
71
+ @data = container
72
+ end
73
+
74
+ def call(inputs, data = {})
75
+ fail "Concrete handlers must implement their own call"
76
+ end
77
+
78
+ def ok(value = nil)
79
+ self.class.ok(value)
80
+ end
81
+
82
+ def error(*args)
83
+ self.class.error(*args)
84
+ end
85
+
86
+ def present(name, data, inputs = {})
87
+ return data if inputs[:raw] == true
88
+
89
+ key = qualify_container_key(name, 'presenters')
90
+ container = self.class.app_container
91
+ unless container.key?(key)
92
+ unless data.respond_to?(:to_h)
93
+ fail "data must implement :to_h for generic presentation"
94
+ end
95
+
96
+ return data.to_h
97
+ end
98
+
99
+ container[key].call(data, inputs)
100
+ end
101
+ end
102
+ end
103
+ end