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.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/lib/upgrow.rb +2 -8
- data/lib/upgrow/action.rb +66 -16
- data/lib/upgrow/actions.rb +31 -0
- data/lib/upgrow/active_record_adapter.rb +24 -15
- data/lib/upgrow/active_record_schema.rb +63 -0
- data/lib/upgrow/basic_model.rb +64 -0
- data/lib/upgrow/basic_repository.rb +49 -22
- data/lib/upgrow/error.rb +19 -0
- data/lib/upgrow/immutable_object.rb +26 -21
- data/lib/upgrow/input.rb +7 -0
- data/lib/upgrow/model.rb +12 -12
- data/lib/upgrow/model_schema.rb +31 -0
- data/lib/upgrow/repository.rb +3 -0
- data/lib/upgrow/result.rb +18 -55
- data/lib/upgrow/schema.rb +33 -0
- data/test/application_system_test_case.rb +11 -0
- data/test/dummy/app/actions/application_action.rb +10 -0
- data/test/dummy/app/actions/articles/create_action.rb +15 -0
- data/test/dummy/app/actions/articles/destroy_action.rb +9 -0
- data/test/dummy/app/actions/articles/edit_action.rb +12 -0
- data/test/dummy/app/actions/articles/index_action.rb +11 -0
- data/test/dummy/app/actions/articles/new_action.rb +8 -0
- data/test/dummy/app/actions/articles/show_action.rb +11 -0
- data/test/dummy/app/actions/articles/update_action.rb +15 -0
- data/test/dummy/app/actions/comments/create_action.rb +15 -0
- data/test/dummy/app/actions/comments/destroy_action.rb +9 -0
- data/test/dummy/app/actions/comments/new_action.rb +8 -0
- data/test/dummy/app/actions/sessions/create_action.rb +23 -0
- data/test/dummy/app/actions/sessions/destroy_action.rb +6 -0
- data/test/dummy/app/actions/sessions/new_action.rb +8 -0
- data/test/dummy/app/actions/user_action.rb +10 -0
- data/test/dummy/app/actions/users/create_action.rb +13 -0
- data/test/dummy/app/actions/users/new_action.rb +8 -0
- data/test/dummy/app/controllers/application_controller.rb +10 -0
- data/test/dummy/app/controllers/articles_controller.rb +32 -28
- data/test/dummy/app/controllers/comments_controller.rb +41 -0
- data/test/dummy/app/controllers/sessions_controller.rb +34 -0
- data/test/dummy/app/controllers/users_controller.rb +29 -0
- data/test/dummy/app/helpers/application_helper.rb +27 -3
- data/test/dummy/app/helpers/users_helper.rb +15 -0
- data/test/dummy/app/inputs/article_input.rb +3 -0
- data/test/dummy/app/inputs/comment_input.rb +11 -0
- data/test/dummy/app/inputs/session_input.rb +9 -0
- data/test/dummy/app/inputs/user_input.rb +9 -0
- data/test/dummy/app/models/article.rb +0 -2
- data/test/dummy/app/models/comment.rb +4 -0
- data/test/dummy/app/models/user.rb +4 -0
- data/test/dummy/app/records/article_record.rb +2 -0
- data/test/dummy/app/records/comment_record.rb +7 -0
- data/test/dummy/app/records/user_record.rb +9 -0
- data/test/dummy/app/repositories/article_repository.rb +26 -0
- data/test/dummy/app/repositories/comment_repository.rb +3 -0
- data/test/dummy/app/repositories/user_repository.rb +12 -0
- data/test/dummy/config/routes.rb +6 -1
- data/test/dummy/db/migrate/20210320140432_create_comments.rb +12 -0
- data/test/dummy/db/migrate/20210409164927_create_users.rb +22 -0
- data/test/dummy/db/schema.rb +24 -1
- data/test/system/articles_test.rb +87 -29
- data/test/system/comments_test.rb +81 -0
- data/test/system/guest_user_test.rb +14 -0
- data/test/system/sign_in_test.rb +57 -0
- data/test/system/sign_out_test.rb +19 -0
- data/test/system/sign_up_test.rb +38 -0
- data/test/test_helper.rb +6 -1
- data/test/upgrow/action_test.rb +101 -9
- data/test/upgrow/actions_test.rb +24 -0
- data/test/upgrow/active_record_adapter_test.rb +12 -17
- data/test/upgrow/active_record_schema_test.rb +92 -0
- data/test/upgrow/basic_model_test.rb +95 -0
- data/test/upgrow/basic_repository_test.rb +48 -27
- data/test/upgrow/immutable_object_test.rb +43 -7
- data/test/upgrow/input_test.rb +19 -1
- data/test/upgrow/model_schema_test.rb +44 -0
- data/test/upgrow/model_test.rb +48 -11
- data/test/upgrow/result_test.rb +19 -64
- data/test/upgrow/schema_test.rb +44 -0
- metadata +128 -50
- data/test/dummy/app/actions/create_article_action.rb +0 -13
- data/test/dummy/app/actions/delete_article_action.rb +0 -7
- data/test/dummy/app/actions/edit_article_action.rb +0 -10
- data/test/dummy/app/actions/list_articles_action.rb +0 -8
- data/test/dummy/app/actions/show_article_action.rb +0 -10
- data/test/dummy/app/actions/update_article_action.rb +0 -13
data/lib/upgrow/error.rb
ADDED
@@ -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
|
-
@
|
9
|
+
@schema = Schema.new
|
7
10
|
|
8
11
|
class << self
|
9
|
-
|
12
|
+
attr_accessor :schema
|
10
13
|
|
11
|
-
#
|
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
|
-
|
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.
|
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
|
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(**
|
36
|
-
absent_attributes =
|
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
|
-
|
43
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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 <
|
20
|
-
|
21
|
-
|
22
|
-
attribute :updated_at
|
22
|
+
class Model < BasicModel
|
23
|
+
class << self
|
24
|
+
private
|
23
25
|
|
24
|
-
|
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
|
-
|
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
|
data/lib/upgrow/repository.rb
CHANGED
@@ -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
|
-
|
30
|
-
|
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
|
-
#
|
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
|
-
# @
|
63
|
-
#
|
39
|
+
# @param values [Hash<Symbol, Object>] the list of values for each member
|
40
|
+
# provided as keyword arguments.
|
64
41
|
#
|
65
|
-
# @return [Result]
|
66
|
-
def
|
67
|
-
|
68
|
-
|
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
|
-
#
|
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
|
-
# @
|
80
|
-
#
|
81
|
-
|
82
|
-
|
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,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,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
|