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