upgrow 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3238cf0308b8e81b0349792dc104880479702dc5c802a8985d2f37461fb1029f
|
4
|
+
data.tar.gz: 3c06e81104d588ae28d500d4db9946e16afeee449eb7b58a70700a8e3f6b7bb1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7f23d2e55b0b06d99c168353c99e1b13e3ee4c0d9a12371e5f6771c7bc9dfae347512c583d716188454b634d54d57149e24b98cf203738d5aee718148405f29
|
7
|
+
data.tar.gz: 646705859b0fb813b215081f1f52058b2b36b2e15b2a0b81cdeb0dfd537ebe30e84c2846a5b37e99d9646e9706015ded057f365100798bdec360918c8936ae5a
|
data/Rakefile
CHANGED
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/
|
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/
|
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 :
|
41
|
+
attr_writer :exposures
|
28
42
|
|
29
|
-
# Each Action class has its own
|
30
|
-
#
|
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 [
|
33
|
-
|
34
|
-
|
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
|
51
|
+
# Sets the given instance variable names as exposed in the Result.
|
39
52
|
#
|
40
|
-
# @param
|
41
|
-
|
42
|
-
|
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
|
-
#
|
47
|
-
# to the Action class's method (see #result_class).
|
68
|
+
# Throws a Result populated with the given errors.
|
48
69
|
#
|
49
|
-
# @
|
50
|
-
|
51
|
-
|
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
|
-
|
13
|
-
|
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
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
61
|
+
def to_model(model_class_or_attributes, attributes = {})
|
62
|
+
model_class = model_class_or_attributes
|
37
63
|
|
38
|
-
|
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
|
-
|
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
|