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