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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'immutable_object'
4
+
5
+ module Upgrow
6
+ # An Error is an all-purpose object that represents a failure in the
7
+ # performance of an Action. Errors are present in failure Results.
8
+ #
9
+ # Error contains a code, which is a machine-friendly unique key that
10
+ # represents the type of error returned. Error also contain a message, which
11
+ # is the human-readable text explaining the particular failure that happened.
12
+ # Optinally Error might also have an attribute value, which is the attribute
13
+ # that caused the failure, in case the error was originated from an Input.
14
+ class Error < ImmutableObject
15
+ attribute :code
16
+ attribute :message
17
+ attribute :attribute
18
+ end
19
+ end
@@ -1,57 +1,62 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require_relative 'schema'
4
+
2
5
  module Upgrow
3
6
  # A read-only Object. An Immutable Object is initialized with its attributes
4
7
  # and subsequent state changes are not permitted.
5
8
  class ImmutableObject
6
- @attribute_names = []
9
+ @schema = Schema.new
7
10
 
8
11
  class << self
9
- attr_reader :attribute_names
12
+ attr_accessor :schema
10
13
 
11
- # Specify an attribute for the Immutable Object. This enables the object
12
- # to be instantiated with the attribute, as well as creates an attribute
13
- # reader for it.
14
+ # Defines an attribute in the Immutable Object Schema.
14
15
  #
15
16
  # @param name [Symbol] the name of the attribute.
16
17
  def attribute(name)
17
- @attribute_names << name
18
- attr_reader(name)
18
+ schema.attribute(name)
19
19
  end
20
20
 
21
21
  private
22
22
 
23
23
  def inherited(subclass)
24
24
  super
25
- subclass.instance_variable_set(:@attribute_names, @attribute_names.dup)
25
+ subclass.schema = @schema.dup
26
26
  end
27
27
  end
28
28
 
29
+ attr_reader :attributes
30
+
29
31
  # Initializes a new Immutable Object with the given member values.
30
32
  #
31
- # @param args [Hash<Symbol, Object>] the list of values for each attribute
32
- # of the Immutable Object.
33
+ # @param attributes [Hash<Symbol, Object>] the list of values for each
34
+ # attribute of the Immutable Object.
33
35
  #
34
36
  # @raise [ArgumentError] if the given argument is not an attribute.
35
- def initialize(**args)
36
- absent_attributes = args.keys - self.class.attribute_names
37
+ def initialize(**attributes)
38
+ absent_attributes = attributes.keys - self.class.schema.attribute_names
37
39
 
38
40
  if absent_attributes.any?
39
41
  raise ArgumentError, "Unknown attribute #{absent_attributes}"
40
42
  end
41
43
 
42
- args.each do |name, value|
43
- instance_variable_set("@#{name}", value)
44
- end
44
+ @attributes = self.class.schema.attribute_names.to_h do |name|
45
+ [name, attributes[name]]
46
+ end.freeze
45
47
 
46
48
  freeze
47
49
  end
48
50
 
49
- # The collection of attributes and their values.
50
- #
51
- # @return [Hash<Symbol, Object>] the collection of attributes and their
52
- # values.
53
- def attributes
54
- self.class.attribute_names.to_h { |name| [name, public_send(name)] }
51
+ private
52
+
53
+ def method_missing(name, *args, &block)
54
+ super unless attributes.include?(name)
55
+ attributes.fetch(name)
56
+ end
57
+
58
+ def respond_to_missing?(name, _include_private = false)
59
+ attributes.include?(name) || super
55
60
  end
56
61
  end
57
62
  end
data/lib/upgrow/input.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_model'
4
+
5
+ require_relative 'error'
6
+ require_relative 'immutable_object'
7
+
3
8
  module Upgrow
4
9
  # Inputs are objects that represent user-entered data. They are populated with
5
10
  # information that is available for modification, such as in HTML forms or in
@@ -37,6 +42,8 @@ module Upgrow
37
42
  super(**attributes.to_hash.transform_keys(&:to_sym))
38
43
  end
39
44
 
45
+ undef_method :validation_context=
46
+
40
47
  private
41
48
 
42
49
  # Overwrites the validation context writer so the Input's state is not
data/lib/upgrow/model.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'active_record_schema'
4
+ require_relative 'basic_model'
5
+
3
6
  module Upgrow
4
7
  # Models are objects that represent core entities of the app’s business logic.
5
8
  # These are usually persisted and can be fetched and created as needed. They
@@ -16,20 +19,17 @@ module Upgrow
16
19
  # Record to be completely hidden away from any other areas of the app. There
17
20
  # are no references to Records in controllers, views, and anywhere else.
18
21
  # Repositories are invoked instead, which in turn return read-only Models.
19
- class Model < ImmutableObject
20
- attribute :id
21
- attribute :created_at
22
- attribute :updated_at
22
+ class Model < BasicModel
23
+ class << self
24
+ private
23
25
 
24
- # Initializes a new Model with the given member values.
25
- #
26
- # @param args [Hash<Symbol, Object>] the list of values for each attribute.
27
- #
28
- # @raise [KeyError] if an attribute is missing in the list of arguments.
29
- def initialize(**args)
30
- self.class.attribute_names.each { |key| args.fetch(key) }
26
+ def inherited(subclass)
27
+ super
31
28
 
32
- super
29
+ subclass.schema = ActiveRecordSchema.new(
30
+ subclass.name + 'Record', subclass.schema
31
+ )
32
+ end
33
33
  end
34
34
  end
35
35
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upgrow
4
+ # The default Schema type for Basic Models. This is a specialized Schema that,
5
+ # in addition to attributes, supports the definition of associations.
6
+ class ModelSchema < Schema
7
+ # Sets the Model Schema's initial state, with an empty Set of association
8
+ # names.
9
+ def initialize
10
+ super
11
+ @association_names = Set.new
12
+ end
13
+
14
+ # Define a Model association. An association is a special type of attribute
15
+ # that is optional upon initialization, but it raises an Association Not
16
+ # Loaded error in case its reader is called when the attribute has not been
17
+ # specified when the Model was instantiated.
18
+ #
19
+ # @param name [Symbol] the name of the association.
20
+ def association(name)
21
+ @association_names += [name]
22
+ end
23
+
24
+ # The list of association names for this Schema.
25
+ #
26
+ # @return [Array<Symbol>] the list of association names.
27
+ def association_names
28
+ @association_names.to_a
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'active_record_adapter'
4
+ require_relative 'basic_repository'
5
+
3
6
  module Upgrow
4
7
  # Repositories are responsible for the persistence layer of the app. They
5
8
  # encapsulate Rails’ Active Record in a subset of simple methods for querying
data/lib/upgrow/result.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require_relative 'immutable_struct'
4
+
2
5
  module Upgrow
3
6
  # Results are special Structs that are generated dynamically to accommodate a
4
7
  # set of pre-defined members. Since different Actions might want to return
@@ -26,69 +29,29 @@ module Upgrow
26
29
  #
27
30
  # @return [Result] the new Result class with the given members.
28
31
  def new(*members)
29
- super(*members, :errors)
30
- end
31
-
32
- # Returns a new Result instance populated with the given values.
33
- #
34
- # @param values [Hash<Symbol, Object>] the list of values for each member
35
- # provided as keyword arguments.
36
- #
37
- # @return [Result] the Result instance populated with the given values.
38
- def success(*values)
39
- new(*values)
40
- end
41
-
42
- # Returns a new Result instance populated with the given errors.
43
- #
44
- # @param errors [ActiveModel::Errors] the errors object to be set as the
45
- # Result errors.
46
- #
47
- # @return [Result] the Result instance populated with the given errors.
48
- def failure(errors)
49
- values = members.to_h { |member| [member, nil] }
50
- new(**values.merge(errors: errors))
32
+ members << :errors unless members.include?(:errors)
33
+ super(*members)
51
34
  end
52
35
  end
53
36
 
54
- # Calls the given block only when the Result is successful.
55
- #
56
- # This method receives a block that is called with the Result values passed
57
- # to the block only when the Result itself is a success, meaning its list of
58
- # errors is empty. Otherwise the block is not called.
59
- #
60
- # It returns self for convenience so other methods can be chained together.
37
+ # Returns a new Result instance populated with the given values.
61
38
  #
62
- # @yield [values] gives the Result values to the block on a successful
63
- # Result.
39
+ # @param values [Hash<Symbol, Object>] the list of values for each member
40
+ # provided as keyword arguments.
64
41
  #
65
- # @return [Result] self for chaining.
66
- def and_then
67
- yield(**to_h.except(:errors)) if errors.none?
68
- self
42
+ # @return [Result] the Result instance populated with the given values.
43
+ def initialize(**values)
44
+ errors = values.fetch(:errors, [])
45
+ super(**values.merge(errors: errors))
69
46
  end
70
47
 
71
- # Calls the given block only when the Result is a failure.
72
- #
73
- # This method receives a block that is called with the Result errors passed
74
- # to the block only when the Result itself is a failure, meaning its list of
75
- # errors is not empty. Otherwise the block is not called.
76
- #
77
- # It returns self for convenience so other methods can be chained together.
48
+ # Check if the Result is successful or not. A successful Result means there
49
+ # are no errors present.
78
50
  #
79
- # @yield [errors] gives the Result errors to the block on a failed Result.
80
- #
81
- # @return [Result] self for chaining.
82
- def or_else
83
- yield(errors) if errors.any?
84
- self
85
- end
86
-
87
- private
88
-
89
- def initialize(*args)
90
- values = { errors: [] }.merge(args.first || {})
91
- super(**values)
51
+ # @return [true] if the Result is successful.
52
+ # @return [false] if the Result is a failure.
53
+ def success?
54
+ errors.none?
92
55
  end
93
56
  end
94
57
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upgrow
4
+ # Defines attribute names to be set in a Model class. This allows pre-defining
5
+ # a set of attributes to be set at once in a Model without having to declare
6
+ # each one by hand.
7
+ #
8
+ # A Schema is a loose concept. This is just a convenience class to be used
9
+ # when a more robust object is not present. In reality, any object that
10
+ # responds to `attribute_names` can be used as a Schema.
11
+ class Schema
12
+ # Sets the Schema's attribute names.
13
+ #
14
+ # @param attribute_names [Array<Symbol>] the attribute names to be set.
15
+ def initialize(*attribute_names)
16
+ @attribute_names = Set.new(attribute_names)
17
+ end
18
+
19
+ # The list of attribute names for this Schema.
20
+ #
21
+ # @return [Array<Symbol>] the list of attribute names.
22
+ def attribute_names
23
+ @attribute_names.to_a
24
+ end
25
+
26
+ # Defines an attribute.
27
+ #
28
+ # @param name [Symbol] the name of the attribute.
29
+ def attribute(name)
30
+ @attribute_names += [name]
31
+ end
32
+ end
33
+ end
@@ -10,4 +10,15 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
10
10
  driven_by :selenium, using: :headless_chrome do |options|
11
11
  options.add_argument('--disable-dev-shm-usage')
12
12
  end
13
+
14
+ def sign_in(email: 'existing@example.com', password: '123xyz')
15
+ visit(root_path)
16
+
17
+ click_link('Sign in')
18
+
19
+ fill_in('Email', with: email)
20
+ fill_in('Password', with: password)
21
+
22
+ click_button('Sign in')
23
+ end
13
24
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationAction < Upgrow::Action
4
+ expose :current_user
5
+
6
+ # rubocop:disable Lint/MissingSuper
7
+ def initialize(user_id:)
8
+ @current_user = UserRepository.new.find_from_context(user_id)
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Articles
4
+ class CreateAction < UserAction
5
+ expose :article
6
+
7
+ def perform(input)
8
+ if input.valid?
9
+ @article = ArticleRepository.new.create(input)
10
+ else
11
+ failure(input.errors)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Articles
4
+ class DestroyAction < UserAction
5
+ def perform(id)
6
+ ArticleRepository.new.delete(id)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Articles
4
+ class EditAction < UserAction
5
+ expose :article
6
+
7
+ def perform(id)
8
+ @article = ArticleRepository.new.find_for_user(id, user: @current_user)
9
+ raise UnauthorizedError unless @article
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Articles
4
+ class IndexAction < ApplicationAction
5
+ expose :articles
6
+
7
+ def perform
8
+ @articles = ArticleRepository.new.all
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Articles
4
+ class NewAction < UserAction
5
+ def perform
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Articles
4
+ class ShowAction < ApplicationAction
5
+ expose :article
6
+
7
+ def perform(id)
8
+ @article = ArticleRepository.new.find_with_comments(id)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Articles
4
+ class UpdateAction < UserAction
5
+ expose :article
6
+
7
+ def perform(id, input)
8
+ if input.valid?
9
+ @article = ArticleRepository.new.update(id, input)
10
+ else
11
+ failure(input.errors)
12
+ end
13
+ end
14
+ end
15
+ end