upgrow 0.0.1 → 0.0.2
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/README.md +45 -12
- data/Rakefile +8 -4
- data/lib/upgrow.rb +13 -2
- data/lib/upgrow/action.rb +54 -0
- data/lib/upgrow/active_record_adapter.rb +73 -0
- data/lib/upgrow/basic_repository.rb +45 -0
- data/lib/upgrow/immutable_object.rb +57 -0
- data/lib/upgrow/immutable_struct.rb +40 -0
- data/lib/upgrow/input.rb +47 -0
- data/lib/upgrow/model.rb +35 -0
- data/lib/upgrow/repository.rb +12 -0
- data/lib/upgrow/result.rb +94 -0
- data/test/application_system_test_case.rb +13 -0
- data/test/dummy/app/actions/create_article_action.rb +13 -0
- data/test/dummy/app/actions/delete_article_action.rb +7 -0
- data/test/dummy/app/actions/edit_article_action.rb +10 -0
- data/test/dummy/app/actions/list_articles_action.rb +8 -0
- data/test/dummy/app/actions/show_article_action.rb +10 -0
- data/test/dummy/app/actions/update_article_action.rb +13 -0
- data/test/dummy/app/channels/application_cable/channel.rb +5 -0
- data/test/dummy/app/channels/application_cable/connection.rb +5 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/articles_controller.rb +64 -0
- data/test/dummy/app/helpers/application_helper.rb +102 -0
- data/test/dummy/app/inputs/article_input.rb +8 -0
- data/test/dummy/app/jobs/application_job.rb +9 -0
- data/test/dummy/app/mailers/application_mailer.rb +5 -0
- data/test/dummy/app/models/article.rb +6 -0
- data/test/dummy/app/records/application_record.rb +5 -0
- data/test/dummy/app/records/article_record.rb +5 -0
- data/test/dummy/app/repositories/article_repository.rb +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +6 -0
- data/test/dummy/config/environment.rb +6 -0
- data/test/dummy/config/environments/development.rb +79 -0
- data/test/dummy/config/environments/production.rb +133 -0
- data/test/dummy/config/environments/test.rb +61 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +9 -0
- data/test/dummy/config/initializers/assets.rb +13 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +12 -0
- data/test/dummy/config/initializers/content_security_policy.rb +31 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +6 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +17 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/permissions_policy.rb +12 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +16 -0
- data/test/dummy/config/puma.rb +44 -0
- data/test/dummy/config/routes.rb +7 -0
- data/test/dummy/db/migrate/20210219211631_create_articles.rb +11 -0
- data/test/dummy/db/schema.rb +22 -0
- data/test/rails_helper.rb +21 -0
- data/test/system/articles_test.rb +109 -0
- data/test/test_helper.rb +3 -3
- data/test/upgrow/action_test.rb +25 -0
- data/test/upgrow/active_record_adapter_test.rb +94 -0
- data/test/upgrow/basic_repository_test.rb +73 -0
- data/test/upgrow/documentation_test.rb +12 -0
- data/test/upgrow/immutable_object_test.rb +60 -0
- data/test/upgrow/immutable_struct_test.rb +49 -0
- data/test/upgrow/input_test.rb +65 -0
- data/test/upgrow/model_test.rb +27 -0
- data/test/upgrow/result_test.rb +95 -0
- metadata +128 -7
- data/test/documentation_test.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ade9c609af4d3b6a3dbcf14e3391c279b0043ca68a7b3abca290c0558d31d7d5
|
4
|
+
data.tar.gz: d7f3ad187cc54e12c7000bbb1bc6e489f160b0d24c9e815c4d338c0654ff2980
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a94d9ab2c7d815c912670e58e234f7052c6c062c54cb2ab6c65a729280db7fb1867ee5314aa6599893a6aabbbebdb8763b2cddf9e5e01056e8a532fa7eef7f7b
|
7
|
+
data.tar.gz: 9ad9d668636f257c12c1a9dec5bd6cfcc4c209feb2f18adaf63717d8c0725535b347025305a391ace8d67e6fffef87827015d93784887cf79ea8e7b37aa70915
|
data/README.md
CHANGED
@@ -1,23 +1,56 @@
|
|
1
|
+
<!--
|
2
|
+
# @title Upgrow
|
3
|
+
-->
|
4
|
+
|
1
5
|
# Upgrow
|
2
6
|
|
3
|
-
|
7
|
+
**Heads Up!**
|
4
8
|
|
5
|
-
|
9
|
+
**This project is a work in progress. We are working on documenting our
|
10
|
+
best practices to help sustainable development for the long term with
|
11
|
+
Ruby on Rails. The Upgrow gem is not yet available and the contents of
|
12
|
+
the guide are still being amended, matured, and reviewed. That being
|
13
|
+
said, feel free to follow the GitHub repo and stay tuned for the
|
14
|
+
latest updates!**
|
6
15
|
|
7
|
-
|
16
|
+
## A sustainable architecture for Ruby on Rails
|
8
17
|
|
9
|
-
|
10
|
-
|
11
|
-
|
18
|
+
Ruby on Rails is the framework of choice for web apps at Shopify. It is an
|
19
|
+
opinionated stack for quick and easy development of apps that need standard
|
20
|
+
persistence with relational databases, an HTTP server, and HTML views.
|
21
|
+
|
22
|
+
By design, Rails does not define conventions for structuring business logic and
|
23
|
+
domain-specific code, leaving developers to define their own architecture and
|
24
|
+
best practices for a sustainable codebase.
|
25
|
+
|
26
|
+
In fast product development teams, budgets and deadlines interfere with this
|
27
|
+
architectural work, leading to poorly written business logic and complicated
|
28
|
+
code that is very hard to maintain long term. Even when developer teams take
|
29
|
+
the time to think about what a good architecture in Rails look like, this work
|
30
|
+
is likely required to be done all over again when a new Rails app needs to be
|
31
|
+
created.
|
12
32
|
|
13
|
-
|
33
|
+
This project aims to make it easier for both new and existing Rails apps to
|
34
|
+
adopt patterns that are proven to make code more sustainable long term, and
|
35
|
+
codebases easier to maintain and extend. We will recommend a set of abstractions
|
36
|
+
and practices that are simple, yet powerful in organizing code in Rails apps in
|
37
|
+
a way that allows fast-growing apps to remain easy to change.
|
14
38
|
|
15
|
-
|
39
|
+
## The Upgrow Guide
|
16
40
|
|
17
|
-
|
41
|
+
Visit [https://upgrow.shopify.io](https://upgrow.shopify.io) to learn more about
|
42
|
+
creating a sustainable architecture for your Rails apps.
|
18
43
|
|
19
|
-
|
44
|
+
## The Upgrow Gem
|
45
|
+
|
46
|
+
This project offers a Ruby gem to make it easier for Rails apps to adopt the
|
47
|
+
sustainable architecture proposed in the Upgrow Guide. To install, add this line
|
48
|
+
to your application's Gemfile and run `bundle install`:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
gem 'upgrow'
|
52
|
+
```
|
20
53
|
|
21
|
-
##
|
54
|
+
## License
|
22
55
|
|
23
|
-
|
56
|
+
For copyright and licensing please refer to `LICENSE.txt`.
|
data/Rakefile
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'bundler/setup'
|
3
4
|
require 'bundler/gem_tasks'
|
4
5
|
require 'rake/testtask'
|
6
|
+
require 'rubocop/rake_task'
|
7
|
+
|
8
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
9
|
+
load('rails/tasks/engine.rake')
|
10
|
+
task 'test:system' => 'app:test:system' # Hack to use the test environment.
|
5
11
|
|
6
12
|
Rake::TestTask.new do |t|
|
7
13
|
t.libs << 'test'
|
14
|
+
t.pattern = 'test/upgrow/**/*_test.rb'
|
8
15
|
t.warning = true
|
9
16
|
t.verbose = true
|
10
|
-
t.test_files = FileList['test/**/*.rb']
|
11
17
|
end
|
12
18
|
|
13
|
-
require 'rubocop/rake_task'
|
14
|
-
|
15
19
|
RuboCop::RakeTask.new
|
16
20
|
|
17
|
-
task default: [
|
21
|
+
task default: ['rubocop', 'test', 'db:setup', 'test:system']
|
data/lib/upgrow.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
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'
|
11
|
+
require_relative 'upgrow/input'
|
12
|
+
require_relative 'upgrow/model'
|
13
|
+
require_relative 'upgrow/result'
|
14
|
+
|
15
|
+
# The gem's main namespace.
|
3
16
|
module Upgrow
|
4
|
-
class Error < StandardError; end
|
5
|
-
# Your code goes here...
|
6
17
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Upgrow
|
4
|
+
# Actions represent the entry points to the app’s core logic. These objects
|
5
|
+
# coordinate workflows in order to get operations and activities done.
|
6
|
+
# Ultimately, Actions are the public interface of the app’s business layers.
|
7
|
+
#
|
8
|
+
# Rails controllers talk to the app’s internals by sending messages to
|
9
|
+
# specific Actions, optionally with the required inputs. Actions have a
|
10
|
+
# one-to-one relationship with incoming requests: they are paired
|
11
|
+
# symmetrically with end-user intents and demands. This is quite a special
|
12
|
+
# requirement from this layer: any given HTTP request handled by the app
|
13
|
+
# should be handled by a single Action.
|
14
|
+
#
|
15
|
+
# The fact that each Action represents a meaningful and complete
|
16
|
+
# request-response cycle forces modularization for the app’s business logic,
|
17
|
+
# exposing immediately complex relationships between objects at the same time
|
18
|
+
# that frees up the app from scenarios such as interdependent requests. In
|
19
|
+
# other words, Actions do not have knowledge or coupling between other Actions
|
20
|
+
# whatsoever.
|
21
|
+
#
|
22
|
+
# Actions respond to a single public method perform. Each Action defines its
|
23
|
+
# own set of required arguments for perform, as well what can be expected as
|
24
|
+
# the result of that method.
|
25
|
+
class Action
|
26
|
+
class << self
|
27
|
+
attr_writer :result_class
|
28
|
+
|
29
|
+
# Each Action class has its own Result class with the expected members to
|
30
|
+
# be returned when the Action is called successfully.
|
31
|
+
#
|
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
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sets the Action Result class with the given members.
|
39
|
+
#
|
40
|
+
# @param args [Array<Symbol>] the list of members for the Result class.
|
41
|
+
def result(*args)
|
42
|
+
@result_class = Result.new(*args)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Instance method to return the Action's Result class. This method delegates
|
47
|
+
# to the Action class's method (see #result_class).
|
48
|
+
#
|
49
|
+
# @return [Result] the Result class for this Action.
|
50
|
+
def result
|
51
|
+
self.class.result_class
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Upgrow
|
4
|
+
# Mixin that implements Repository methods with an Active Record Base. When
|
5
|
+
# included in a Repository class, it sets the default base to be a class
|
6
|
+
# ending with `Record`.
|
7
|
+
module ActiveRecordAdapter
|
8
|
+
# Fetches all Records and returns them as an Array of Models.
|
9
|
+
#
|
10
|
+
# @return [Array<Model>] a collection of Models representing all persisted
|
11
|
+
# Records.
|
12
|
+
def all
|
13
|
+
base.all.map { |record| to_model(record.attributes) }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Persists a new Record with the given input, and materializes the newly
|
17
|
+
# created Record as the returned Model instance.
|
18
|
+
#
|
19
|
+
# @param input [Input] the Input with the attributes for the new Record.
|
20
|
+
#
|
21
|
+
# @return [Model] the Model with the attributes of the newly created
|
22
|
+
# Record.
|
23
|
+
def create(input)
|
24
|
+
record = base.create!(input.attributes)
|
25
|
+
to_model(record.attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Retrieves the Record with the given ID, representing its data as a Model.
|
29
|
+
#
|
30
|
+
# @param id [Integer] the ID of the Record to be fetched.
|
31
|
+
#
|
32
|
+
# @return [Model] the Model with the attributes of the Record with the given
|
33
|
+
# ID.
|
34
|
+
def find(id)
|
35
|
+
record = base.find(id)
|
36
|
+
to_model(record.attributes)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Updates the Record with the given ID with the given Input attributes.
|
40
|
+
#
|
41
|
+
# @param id [Integer] the ID of the Record to be updated.
|
42
|
+
# @param input [Input] the Input with the attributes to be set in the
|
43
|
+
# Record.
|
44
|
+
#
|
45
|
+
# @return [Model] the Model instance with the updated data of the Record.
|
46
|
+
def update(id, input)
|
47
|
+
record = base.update(id, input.attributes)
|
48
|
+
to_model(record.attributes)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Deletes the Record that has the given ID.
|
52
|
+
#
|
53
|
+
# @param id [Integer] the ID of the Record to be deleted.
|
54
|
+
def delete(id)
|
55
|
+
base.destroy(id)
|
56
|
+
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
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Upgrow
|
4
|
+
# Base class for Repositories. It offers a basic API for the state all
|
5
|
+
# Repositories should have, as well as the logic on how to materialize data
|
6
|
+
# into Models.
|
7
|
+
class BasicRepository
|
8
|
+
attr_reader :base, :model_class
|
9
|
+
|
10
|
+
# 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
|
23
|
+
end
|
24
|
+
|
25
|
+
# Represents the raw Hash of data attributes as a Model instance from the
|
26
|
+
# Repositorie's Model class.
|
27
|
+
#
|
28
|
+
# @param attributes [Hash<Symbol, Object>] the list of attributes the Model
|
29
|
+
# will have.
|
30
|
+
#
|
31
|
+
# @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
|
37
|
+
|
38
|
+
def default_base; end
|
39
|
+
|
40
|
+
def default_model_class
|
41
|
+
model_class_name = self.class.name[/\A(.+)Repository\z/, 1]
|
42
|
+
Object.const_get(model_class_name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Upgrow
|
3
|
+
# A read-only Object. An Immutable Object is initialized with its attributes
|
4
|
+
# and subsequent state changes are not permitted.
|
5
|
+
class ImmutableObject
|
6
|
+
@attribute_names = []
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :attribute_names
|
10
|
+
|
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
|
+
#
|
15
|
+
# @param name [Symbol] the name of the attribute.
|
16
|
+
def attribute(name)
|
17
|
+
@attribute_names << name
|
18
|
+
attr_reader(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def inherited(subclass)
|
24
|
+
super
|
25
|
+
subclass.instance_variable_set(:@attribute_names, @attribute_names.dup)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Initializes a new Immutable Object with the given member values.
|
30
|
+
#
|
31
|
+
# @param args [Hash<Symbol, Object>] the list of values for each attribute
|
32
|
+
# of the Immutable Object.
|
33
|
+
#
|
34
|
+
# @raise [ArgumentError] if the given argument is not an attribute.
|
35
|
+
def initialize(**args)
|
36
|
+
absent_attributes = args.keys - self.class.attribute_names
|
37
|
+
|
38
|
+
if absent_attributes.any?
|
39
|
+
raise ArgumentError, "Unknown attribute #{absent_attributes}"
|
40
|
+
end
|
41
|
+
|
42
|
+
args.each do |name, value|
|
43
|
+
instance_variable_set("@#{name}", value)
|
44
|
+
end
|
45
|
+
|
46
|
+
freeze
|
47
|
+
end
|
48
|
+
|
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)] }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Upgrow
|
3
|
+
# A read-only Struct. An Immutable Struct is initialized with its member
|
4
|
+
# values and subsequent state changes are not permitted.
|
5
|
+
class ImmutableStruct < Struct
|
6
|
+
class << self
|
7
|
+
# Creates a new Immutable Struct class with the given members.
|
8
|
+
#
|
9
|
+
# @param args [Array<Symbol>] the list of members for the new class.
|
10
|
+
#
|
11
|
+
# @return [ImmutableStruct] the new Immutable Struct class able to
|
12
|
+
# accommodate the given members.
|
13
|
+
def new(*args, &block)
|
14
|
+
if args.any? { |member| !member.is_a?(Symbol) }
|
15
|
+
raise ArgumentError, 'all members must be symbols'
|
16
|
+
end
|
17
|
+
|
18
|
+
struct_class = super(*args, keyword_init: true, &block)
|
19
|
+
|
20
|
+
struct_class.members.each do |member|
|
21
|
+
struct_class.send(:undef_method, :"#{member}=")
|
22
|
+
end
|
23
|
+
|
24
|
+
struct_class
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
undef []=
|
29
|
+
|
30
|
+
# Initializes a new Immutable Struct with the given member values.
|
31
|
+
#
|
32
|
+
# @param args [Hash<Symbol, Object>] the list of values for each member of
|
33
|
+
# the Immutable Struct.
|
34
|
+
def initialize(**args)
|
35
|
+
members.each { |key| args.fetch(key) }
|
36
|
+
super(**args)
|
37
|
+
freeze
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/upgrow/input.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Upgrow
|
4
|
+
# Inputs are objects that represent user-entered data. They are populated with
|
5
|
+
# information that is available for modification, such as in HTML forms or in
|
6
|
+
# API payloads, and they are passed on to Repositories as arguments for
|
7
|
+
# persistence operations, as provided by the app user. Inputs have knowledge
|
8
|
+
# about which attributes should be present in a payload, among other
|
9
|
+
# constraints, and are able to tell if its own state is valid or not.
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# It is important to note that Inputs differ from Records for not representing
|
13
|
+
# domain entities, but simply data entered by the user. Inputs do not have
|
14
|
+
# numeric identifiers, for example, as these are generated by the system and
|
15
|
+
# not set by users. There are also no strong expectations in regards to data
|
16
|
+
# integrity for inputs, since user-entered data can contain any information of
|
17
|
+
# different types, or even not to be present at all.
|
18
|
+
#
|
19
|
+
# User input validation is a core part of any app’s business logic. It ensures
|
20
|
+
# that incoming data is sane, proper, and respects a predefined schema. A
|
21
|
+
# default Rails app overloads Record objects with yet another responsibility:
|
22
|
+
# being the place where validation rules are written and checked. While there
|
23
|
+
# is value in making sure that database constraints are respected, input
|
24
|
+
# validation should happen as part of the business logic layer, before
|
25
|
+
# persistence is invoked with invalid input. Input objects are a great fit for
|
26
|
+
# that task. By leveraging validation utilities from Active Model, Input
|
27
|
+
# objects can not only perform the same validations as Records but also
|
28
|
+
# seamlessly integrate with view helpers such as Rails form builders.
|
29
|
+
class Input < ImmutableObject
|
30
|
+
include ActiveModel::Validations
|
31
|
+
|
32
|
+
# Creates a new Input instance.
|
33
|
+
#
|
34
|
+
# @param attributes [Hash<String, Object>] the values for each attribute.
|
35
|
+
def initialize(attributes = {})
|
36
|
+
@errors = ActiveModel::Errors.new(self)
|
37
|
+
super(**attributes.to_hash.transform_keys(&:to_sym))
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Overwrites the validation context writer so the Input's state is not
|
43
|
+
# mutated.
|
44
|
+
def validation_context=(_)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|