upgrow 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/lib/upgrow.rb +2 -8
  4. data/lib/upgrow/action.rb +66 -16
  5. data/lib/upgrow/actions.rb +31 -0
  6. data/lib/upgrow/active_record_adapter.rb +24 -15
  7. data/lib/upgrow/active_record_schema.rb +63 -0
  8. data/lib/upgrow/basic_model.rb +64 -0
  9. data/lib/upgrow/basic_repository.rb +49 -22
  10. data/lib/upgrow/error.rb +19 -0
  11. data/lib/upgrow/immutable_object.rb +26 -21
  12. data/lib/upgrow/input.rb +7 -0
  13. data/lib/upgrow/model.rb +12 -12
  14. data/lib/upgrow/model_schema.rb +31 -0
  15. data/lib/upgrow/repository.rb +3 -0
  16. data/lib/upgrow/result.rb +18 -55
  17. data/lib/upgrow/schema.rb +33 -0
  18. data/test/application_system_test_case.rb +11 -0
  19. data/test/dummy/app/actions/application_action.rb +10 -0
  20. data/test/dummy/app/actions/articles/create_action.rb +15 -0
  21. data/test/dummy/app/actions/articles/destroy_action.rb +9 -0
  22. data/test/dummy/app/actions/articles/edit_action.rb +12 -0
  23. data/test/dummy/app/actions/articles/index_action.rb +11 -0
  24. data/test/dummy/app/actions/articles/new_action.rb +8 -0
  25. data/test/dummy/app/actions/articles/show_action.rb +11 -0
  26. data/test/dummy/app/actions/articles/update_action.rb +15 -0
  27. data/test/dummy/app/actions/comments/create_action.rb +15 -0
  28. data/test/dummy/app/actions/comments/destroy_action.rb +9 -0
  29. data/test/dummy/app/actions/comments/new_action.rb +8 -0
  30. data/test/dummy/app/actions/sessions/create_action.rb +23 -0
  31. data/test/dummy/app/actions/sessions/destroy_action.rb +6 -0
  32. data/test/dummy/app/actions/sessions/new_action.rb +8 -0
  33. data/test/dummy/app/actions/user_action.rb +10 -0
  34. data/test/dummy/app/actions/users/create_action.rb +13 -0
  35. data/test/dummy/app/actions/users/new_action.rb +8 -0
  36. data/test/dummy/app/controllers/application_controller.rb +10 -0
  37. data/test/dummy/app/controllers/articles_controller.rb +32 -28
  38. data/test/dummy/app/controllers/comments_controller.rb +41 -0
  39. data/test/dummy/app/controllers/sessions_controller.rb +34 -0
  40. data/test/dummy/app/controllers/users_controller.rb +29 -0
  41. data/test/dummy/app/helpers/application_helper.rb +27 -3
  42. data/test/dummy/app/helpers/users_helper.rb +15 -0
  43. data/test/dummy/app/inputs/article_input.rb +3 -0
  44. data/test/dummy/app/inputs/comment_input.rb +11 -0
  45. data/test/dummy/app/inputs/session_input.rb +9 -0
  46. data/test/dummy/app/inputs/user_input.rb +9 -0
  47. data/test/dummy/app/models/article.rb +0 -2
  48. data/test/dummy/app/models/comment.rb +4 -0
  49. data/test/dummy/app/models/user.rb +4 -0
  50. data/test/dummy/app/records/article_record.rb +2 -0
  51. data/test/dummy/app/records/comment_record.rb +7 -0
  52. data/test/dummy/app/records/user_record.rb +9 -0
  53. data/test/dummy/app/repositories/article_repository.rb +26 -0
  54. data/test/dummy/app/repositories/comment_repository.rb +3 -0
  55. data/test/dummy/app/repositories/user_repository.rb +12 -0
  56. data/test/dummy/config/routes.rb +6 -1
  57. data/test/dummy/db/migrate/20210320140432_create_comments.rb +12 -0
  58. data/test/dummy/db/migrate/20210409164927_create_users.rb +22 -0
  59. data/test/dummy/db/schema.rb +24 -1
  60. data/test/system/articles_test.rb +87 -29
  61. data/test/system/comments_test.rb +81 -0
  62. data/test/system/guest_user_test.rb +14 -0
  63. data/test/system/sign_in_test.rb +57 -0
  64. data/test/system/sign_out_test.rb +19 -0
  65. data/test/system/sign_up_test.rb +38 -0
  66. data/test/test_helper.rb +6 -1
  67. data/test/upgrow/action_test.rb +101 -9
  68. data/test/upgrow/actions_test.rb +24 -0
  69. data/test/upgrow/active_record_adapter_test.rb +12 -17
  70. data/test/upgrow/active_record_schema_test.rb +92 -0
  71. data/test/upgrow/basic_model_test.rb +95 -0
  72. data/test/upgrow/basic_repository_test.rb +48 -27
  73. data/test/upgrow/immutable_object_test.rb +43 -7
  74. data/test/upgrow/input_test.rb +19 -1
  75. data/test/upgrow/model_schema_test.rb +44 -0
  76. data/test/upgrow/model_test.rb +48 -11
  77. data/test/upgrow/result_test.rb +19 -64
  78. data/test/upgrow/schema_test.rb +44 -0
  79. metadata +128 -50
  80. data/test/dummy/app/actions/create_article_action.rb +0 -13
  81. data/test/dummy/app/actions/delete_article_action.rb +0 -7
  82. data/test/dummy/app/actions/edit_article_action.rb +0 -10
  83. data/test/dummy/app/actions/list_articles_action.rb +0 -8
  84. data/test/dummy/app/actions/show_article_action.rb +0 -10
  85. data/test/dummy/app/actions/update_article_action.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ade9c609af4d3b6a3dbcf14e3391c279b0043ca68a7b3abca290c0558d31d7d5
4
- data.tar.gz: d7f3ad187cc54e12c7000bbb1bc6e489f160b0d24c9e815c4d338c0654ff2980
3
+ metadata.gz: 3238cf0308b8e81b0349792dc104880479702dc5c802a8985d2f37461fb1029f
4
+ data.tar.gz: 3c06e81104d588ae28d500d4db9946e16afeee449eb7b58a70700a8e3f6b7bb1
5
5
  SHA512:
6
- metadata.gz: a94d9ab2c7d815c912670e58e234f7052c6c062c54cb2ab6c65a729280db7fb1867ee5314aa6599893a6aabbbebdb8763b2cddf9e5e01056e8a532fa7eef7f7b
7
- data.tar.gz: 9ad9d668636f257c12c1a9dec5bd6cfcc4c209feb2f18adaf63717d8c0725535b347025305a391ace8d67e6fffef87827015d93784887cf79ea8e7b37aa70915
6
+ metadata.gz: b7f23d2e55b0b06d99c168353c99e1b13e3ee4c0d9a12371e5f6771c7bc9dfae347512c583d716188454b634d54d57149e24b98cf203738d5aee718148405f29
7
+ data.tar.gz: 646705859b0fb813b215081f1f52058b2b36b2e15b2a0b81cdeb0dfd537ebe30e84c2846a5b37e99d9646e9706015ded057f365100798bdec360918c8936ae5a
data/Rakefile CHANGED
@@ -18,4 +18,4 @@ end
18
18
 
19
19
  RuboCop::RakeTask.new
20
20
 
21
- task default: ['rubocop', 'test', 'db:setup', 'test:system']
21
+ task default: ['rubocop', 'test', 'test:system']
data/lib/upgrow.rb CHANGED
@@ -1,16 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_model'
4
-
5
3
  require_relative 'upgrow/action'
6
- require_relative 'upgrow/active_record_adapter'
7
- require_relative 'upgrow/immutable_object'
8
- require_relative 'upgrow/basic_repository'
9
- require_relative 'upgrow/immutable_struct'
10
- require_relative 'upgrow/repository'
4
+ require_relative 'upgrow/actions'
11
5
  require_relative 'upgrow/input'
12
6
  require_relative 'upgrow/model'
13
- require_relative 'upgrow/result'
7
+ require_relative 'upgrow/repository'
14
8
 
15
9
  # The gem's main namespace.
16
10
  module Upgrow
data/lib/upgrow/action.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'result'
4
+
3
5
  module Upgrow
4
6
  # Actions represent the entry points to the app’s core logic. These objects
5
7
  # coordinate workflows in order to get operations and activities done.
@@ -23,32 +25,80 @@ module Upgrow
23
25
  # own set of required arguments for perform, as well what can be expected as
24
26
  # the result of that method.
25
27
  class Action
28
+ # Module to be prepended to subclasses of Action. This allows Action to
29
+ # decorate methods implemented by subclasses so they can have additional
30
+ # behaviour.
31
+ module Decorator
32
+ def perform(...)
33
+ catch(:failure) do
34
+ super
35
+ result
36
+ end
37
+ end
38
+ end
39
+
26
40
  class << self
27
- attr_writer :result_class
41
+ attr_writer :exposures
28
42
 
29
- # Each Action class has its own Result class with the expected members to
30
- # be returned when the Action is called successfully.
43
+ # Each Action class has its own list of exposed instance variables to be
44
+ # included in the returned Result when the Action is called.
31
45
  #
32
- # @return [Result] the Result class for this Action, as previously
33
- # defined, or a Result class with no members by default.
34
- def result_class
35
- @result_class ||= Result.new
46
+ # @return [Array] the list of exposed instance variables.
47
+ def exposures
48
+ @exposures ||= []
36
49
  end
37
50
 
38
- # Sets the Action Result class with the given members.
51
+ # Sets the given instance variable names as exposed in the Result.
39
52
  #
40
- # @param args [Array<Symbol>] the list of members for the Result class.
41
- def result(*args)
42
- @result_class = Result.new(*args)
53
+ # @param names [Array<Symbol>] the list of instance variable names to be
54
+ # exposed as part of the Result.
55
+ def expose(*names)
56
+ @exposures += names
57
+ end
58
+
59
+ private
60
+
61
+ def inherited(subclass)
62
+ super
63
+ subclass.exposures = exposures.dup
64
+ subclass.prepend(Decorator)
43
65
  end
44
66
  end
45
67
 
46
- # Instance method to return the Action's Result class. This method delegates
47
- # to the Action class's method (see #result_class).
68
+ # Throws a Result populated with the given errors.
48
69
  #
49
- # @return [Result] the Result class for this Action.
50
- def result
51
- self.class.result_class
70
+ # @param errors [Array<Error>, ActiveModel::Errors] the errors object to
71
+ # be set as the Result errors. If an ActiveModel::Errors object is
72
+ # provided, it will be converted to an Array of Errors.
73
+ #
74
+ # @return [:failure, Result] the Result instance populated with the given
75
+ # errors.
76
+ def failure(*errors)
77
+ errors = errors.first if errors.first.respond_to?(:each)
78
+
79
+ if errors.respond_to?(:full_message)
80
+ errors = errors.map do |error|
81
+ Error.new(
82
+ attribute: error.attribute,
83
+ code: error.type,
84
+ message: error.full_message
85
+ )
86
+ end
87
+ end
88
+
89
+ throw(:failure, result(errors: errors))
90
+ end
91
+
92
+ private
93
+
94
+ def result(members = {})
95
+ result_class = Result.new(*self.class.exposures)
96
+
97
+ values = self.class.exposures.to_h do |name|
98
+ [name, instance_variable_get("@#{name}")]
99
+ end
100
+
101
+ result_class.new(**values.merge(members))
52
102
  end
53
103
  end
54
104
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upgrow
4
+ # This module offers helpers to work with the collection of Actions loaded in
5
+ # the application.
6
+ #
7
+ # Actions are created with a concrete, predefined interface. This allows them
8
+ # to be uniformly instantiated in the context of an application, which is
9
+ # helpful to prevent duplications.
10
+ module Actions
11
+ extend self
12
+
13
+ # Convenience method to retrieve an Action based on a key name optionally
14
+ # namespaced.
15
+ #
16
+ # @example Retrieve Action based on a global key name
17
+ # Actions['show'] #=> ShowAction
18
+ #
19
+ # @example Retrieve a namespaced Action
20
+ # Actions['articles', 'show'] #=> Articles::ShowAction
21
+ #
22
+ # @param names [Array<String>] one or more names, the last one being the
23
+ # name of the Action without the "Action" suffix, optionally lowercased.
24
+ #
25
+ # @return [Action] the Action specified by the names.
26
+ def [](*names)
27
+ action_class_name = names.map(&:capitalize).join('::') + 'Action'
28
+ Object.const_get(action_class_name)
29
+ end
30
+ end
31
+ end
@@ -5,6 +5,30 @@ module Upgrow
5
5
  # included in a Repository class, it sets the default base to be a class
6
6
  # ending with `Record`.
7
7
  module ActiveRecordAdapter
8
+ # Class methods for classes that include this module.
9
+ module ClassMethods
10
+ # Callback method used by Basic Repository to set a default Repository
11
+ # base when one is not explicitly provided at the Repository
12
+ # initialization.
13
+ #
14
+ # It attempts to find a constant based on the Repository name, with the
15
+ # `Record` suffix as a convention. For example, a `UserRepository` would
16
+ # have the `UserRecord` as its base. That is the naming convention for
17
+ # Active Record classes under this architecture.
18
+ #
19
+ # @return [Class] the Active Record Base class to be used as the
20
+ # Repository base according to the architecture's naming convention.
21
+ def default_base
22
+ base_name = name[/\A(.+)Repository\z/, 1] + 'Record'
23
+ Object.const_get(base_name)
24
+ end
25
+ end
26
+
27
+ # @private
28
+ def self.included(base)
29
+ base.extend(ClassMethods)
30
+ end
31
+
8
32
  # Fetches all Records and returns them as an Array of Models.
9
33
  #
10
34
  # @return [Array<Model>] a collection of Models representing all persisted
@@ -54,20 +78,5 @@ module Upgrow
54
78
  def delete(id)
55
79
  base.destroy(id)
56
80
  end
57
-
58
- # Callback method used by Basic Repository to set a default Repository base
59
- # when one is not explicitly provided at the Repository initialization.
60
- #
61
- # It attempts to find a constant based on the Repository name, with the
62
- # `Record` suffix as a convention. For example, a `UserRepository` would
63
- # have the `UserRecord` as its base. That is the naming convention for
64
- # Active Record classes under this architecture.
65
- #
66
- # @return [Class] the Active Record Base class to be used as the Repository
67
- # base according to the architecture's naming convention.
68
- def default_base
69
- base_name = self.class.name[/\A(.+)Repository\z/, 1] + 'Record'
70
- Object.const_get(base_name)
71
- end
72
81
  end
73
82
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upgrow
4
+ # A Schema that dynamically infers attribute names from an Active Record Base,
5
+ # plus the attributes from an original Schema.
6
+ #
7
+ # This object is used to power Models so they can include Active Record
8
+ # attributes from a matching class based on the Model's name.
9
+ class ActiveRecordSchema
10
+ # Set the Schema's initial state.
11
+ #
12
+ # @param base_name [String] the name of the Active Record Base class that
13
+ # should be used to fetch attribute names.
14
+ # @param default_schema [ModelSchema] the original Model Schema to be used
15
+ # to fetch and define custom attributes.
16
+ def initialize(base_name, default_schema)
17
+ @base_name = base_name
18
+ @default_schema = default_schema
19
+ end
20
+
21
+ # Define a custom attribute in the default Schema.
22
+ #
23
+ # @param name [Symbol] the name of the new attribute.
24
+ def attribute(name)
25
+ @default_schema.attribute(name)
26
+ end
27
+
28
+ # Define a custom association in the default Schema.
29
+ #
30
+ # @param name [Symbol] the name of the association.
31
+ def association(name)
32
+ @default_schema.association(name)
33
+ end
34
+
35
+ # The list of attribute names. This is an aggregate of both the attributes
36
+ # from the Active Record Base as well as any custom attributes from the
37
+ # default Schema.
38
+ #
39
+ # @return [Array<Symbol>] the list of attribute names.
40
+ def attribute_names
41
+ base.attribute_names.map(&:to_sym) | @default_schema.attribute_names
42
+ end
43
+
44
+ # The list of association names. This is an aggregate of both the
45
+ # associations from the Active Record Base as well as any custom
46
+ # associations from the default Schema.
47
+ #
48
+ # @return [Array<Symbol>] the list of attribute names.
49
+ def association_names
50
+ association_names = base.reflections.keys.map do |key|
51
+ key.sub('_record', '').to_sym
52
+ end
53
+
54
+ association_names | @default_schema.association_names
55
+ end
56
+
57
+ private
58
+
59
+ def base
60
+ Object.const_get(@base_name)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'immutable_object'
4
+ require_relative 'model_schema'
5
+
6
+ module Upgrow
7
+ # Base class for Models. As an Immutable Object, it sets a default schema with
8
+ # the minimal attribute ID, as well as requires all attributes to be set upon
9
+ # initialization.
10
+ class BasicModel < ImmutableObject
11
+ class AssociationNotLoadedError < StandardError; end
12
+
13
+ class << self
14
+ # Defines an association in the Model Schema.
15
+ #
16
+ # @param name [Symbol] the name of the association.
17
+ def belongs_to(name)
18
+ schema.association(name)
19
+ end
20
+
21
+ # Defines an association in the Model Schema.
22
+ #
23
+ # @param name [Symbol] the name of the association.
24
+ def has_many(name)
25
+ schema.association(name)
26
+ end
27
+ end
28
+
29
+ self.schema = ModelSchema.new
30
+
31
+ attribute :id
32
+
33
+ attr_reader :associations
34
+
35
+ # Initializes a new Model with the given member values.
36
+ #
37
+ # @param args [Hash<Symbol, Object>] the list of values for each attribute
38
+ # and association.
39
+ #
40
+ # @raise [KeyError] if an attribute is missing in the list of arguments.
41
+ def initialize(**args)
42
+ @associations = self.class.schema.association_names.to_h do |name|
43
+ [name, args[name]]
44
+ end.freeze
45
+
46
+ attributes = self.class.schema.attribute_names.to_h do |name|
47
+ [name, args.fetch(name)]
48
+ end
49
+
50
+ super(**attributes)
51
+ end
52
+
53
+ private
54
+
55
+ def method_missing(name, *args, &block)
56
+ return super unless associations.include?(name)
57
+ associations[name] || raise(AssociationNotLoadedError)
58
+ end
59
+
60
+ def respond_to_missing?(name, _include_private = false)
61
+ associations.include?(name) || super
62
+ end
63
+ end
64
+ end
@@ -5,41 +5,68 @@ module Upgrow
5
5
  # Repositories should have, as well as the logic on how to materialize data
6
6
  # into Models.
7
7
  class BasicRepository
8
+ class << self
9
+ attr_writer :base
10
+ attr_writer :model_class
11
+
12
+ # model_class [Class] the Model class to be used to map and return the
13
+ # materialized data as instances of the domain. Defaults to a constant
14
+ # derived from the Repository class' name. For example, a `UserRepository`
15
+ # will have its default Model class set to `User`.
16
+ #
17
+ # @return [Class] the Repository Model class.
18
+ def model_class
19
+ @model_class || default_model_class
20
+ end
21
+
22
+ # the base object to be used internally to retrieve the persisted data.
23
+ # For example, a base class in which queries can be performed for a
24
+ # relational database adapter. Defaults to `nil`.
25
+ #
26
+ # @return [Object] the Repository base.
27
+ def base
28
+ @base || default_base
29
+ end
30
+
31
+ private
32
+
33
+ def default_base; end
34
+
35
+ def default_model_class
36
+ model_class_name = name.delete_suffix('Repository')
37
+ Object.const_get(model_class_name)
38
+ end
39
+ end
40
+
8
41
  attr_reader :base, :model_class
9
42
 
10
43
  # Sets the Basic Repositorie's state.
11
- #
12
- # @param base [Object] the base object to be used internally to retrieve the
13
- # persisted data. For example, a base class in which queries can be
14
- # performed for a relational database adapter. Defaults to `nil`.
15
- #
16
- # @param model_class [Class] the Model class to be used to map and return
17
- # the materialized data as instances of the domain. Defaults to a constant
18
- # derived from the Repository class' name. For example, a `UserRepository`
19
- # will have its default Model class set to `User`.
20
- def initialize(base: default_base, model_class: default_model_class)
21
- @base = base
22
- @model_class = model_class
44
+ def initialize
45
+ @base = self.class.base
46
+ @model_class = self.class.model_class
23
47
  end
24
48
 
25
49
  # Represents the raw Hash of data attributes as a Model instance from the
26
50
  # Repositorie's Model class.
27
51
  #
52
+ # @param model_class_or_attributes [Class, Hash<Symbol, Object>] the Model
53
+ # class to be instantiated, in case it is a different class than the
54
+ # Repositorie's Model class, or the list of attributes the model will
55
+ # have, in case the Model class is the Repositorie's Model class.
28
56
  # @param attributes [Hash<Symbol, Object>] the list of attributes the Model
29
- # will have.
57
+ # will have, in case the Model to be instantiated is passed as the first
58
+ # argument.
30
59
  #
31
60
  # @return [Model] the Model instance populated with the given attributes.
32
- def to_model(attributes = {})
33
- model_class.new(**attributes.transform_keys(&:to_sym))
34
- end
35
-
36
- private
61
+ def to_model(model_class_or_attributes, attributes = {})
62
+ model_class = model_class_or_attributes
37
63
 
38
- def default_base; end
64
+ if model_class_or_attributes.respond_to?(:transform_keys)
65
+ model_class = self.model_class
66
+ attributes = model_class_or_attributes
67
+ end
39
68
 
40
- def default_model_class
41
- model_class_name = self.class.name[/\A(.+)Repository\z/, 1]
42
- Object.const_get(model_class_name)
69
+ model_class.new(**attributes.transform_keys(&:to_sym))
43
70
  end
44
71
  end
45
72
  end