upgrow 0.0.2 → 0.0.3

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