solid-process 0.3.0 → 0.5.0
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/.claude/CLAUDE.md +112 -0
- data/CHANGELOG.md +92 -1
- data/README.md +208 -23
- data/Rakefile +28 -8
- data/docs/010_KEY_CONCEPTS.md +31 -0
- data/docs/020_BASIC_USAGE.md +46 -0
- data/docs/030_INTERMEDIATE_USAGE.md +74 -0
- data/docs/040_ADVANCED_USAGE.md +96 -0
- data/docs/050_ERROR_HANDLING.md +3 -0
- data/docs/060_TESTING.md +3 -0
- data/docs/070_INSTRUMENTATION.md +3 -0
- data/docs/080_RAILS_INTEGRATION.md +3 -0
- data/docs/090_INTERNAL_LIBRARIES.md +3 -0
- data/docs/100_PORTS_AND_ADAPTERS.md +3 -0
- data/lib/solid/input.rb +0 -2
- data/lib/solid/model.rb +19 -7
- data/lib/solid/output.rb +13 -0
- data/lib/solid/process/backtrace_cleaner.rb +17 -0
- data/lib/solid/process/caller.rb +1 -1
- data/lib/solid/process/event_logs/basic_logger_listener.rb +77 -0
- data/lib/solid/process/event_logs.rb +7 -0
- data/lib/solid/process/version.rb +1 -1
- data/lib/solid/process.rb +13 -15
- data/lib/solid/validators/email_validator.rb +4 -2
- data/lib/solid/validators/id_validator.rb +17 -0
- data/lib/solid/validators/instance_of_validator.rb +5 -3
- data/lib/solid/validators/is_validator.rb +19 -0
- data/lib/solid/validators/kind_of_validator.rb +5 -3
- data/lib/solid/validators/respond_to_validator.rb +5 -3
- data/lib/solid/validators/singleton_validator.rb +8 -8
- data/lib/solid/validators/uuid_validator.rb +4 -4
- data/lib/solid/validators.rb +13 -0
- data/lib/solid/value.rb +41 -0
- metadata +26 -16
- data/lib/solid/validators/all.rb +0 -12
- data/lib/solid/validators/bool_validator.rb +0 -9
- data/lib/solid/validators/is_a_validator.rb +0 -6
- data/lib/solid/validators/persisted_validator.rb +0 -9
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<small>
|
|
2
|
+
|
|
3
|
+
`Previous` [Basic Usage](./020_BASIC_USAGE.md) | `Next` [Advanced Usage](./040_ADVANCED_USAGE.md)
|
|
4
|
+
|
|
5
|
+
</small>
|
|
6
|
+
|
|
7
|
+
# Intermediate Usage
|
|
8
|
+
|
|
9
|
+
**Status:** 🟡 `in-progress`
|
|
10
|
+
|
|
11
|
+
In this section, we will learn how to use steps to express the process in a more structured way.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
class User::Registration < Solid::Process
|
|
15
|
+
input do
|
|
16
|
+
attribute :email, :string
|
|
17
|
+
attribute :password, :string
|
|
18
|
+
attribute :password_confirmation, :string
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(attributes)
|
|
22
|
+
rollback_on_failure {
|
|
23
|
+
Given(attributes)
|
|
24
|
+
.and_then(:create_user)
|
|
25
|
+
.and_then(:create_user_account)
|
|
26
|
+
.and_then(:create_user_inbox)
|
|
27
|
+
.and_then(:create_user_token)
|
|
28
|
+
}
|
|
29
|
+
.and_then(:send_email_confirmation)
|
|
30
|
+
.and_expose(:user_registered, [:user])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def create_user(email:, password:, password_confirmation:, **)
|
|
36
|
+
user = User.create(email:, password:, password_confirmation:)
|
|
37
|
+
|
|
38
|
+
return Continue(user:) if user.persisted?
|
|
39
|
+
|
|
40
|
+
input.errors.merge!(user.errors)
|
|
41
|
+
|
|
42
|
+
Failure(:invalid_input, input:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_user_account(user:, **)
|
|
46
|
+
account = Account.create!(uuid: SecureRandom.uuid)
|
|
47
|
+
|
|
48
|
+
account.memberships.create!(user:, role: :owner)
|
|
49
|
+
|
|
50
|
+
Continue(account:)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_user_inbox(account:, **)
|
|
54
|
+
account.task_lists.inbox.create!
|
|
55
|
+
|
|
56
|
+
Continue()
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def create_user_token(user:, **)
|
|
60
|
+
user.create_token!
|
|
61
|
+
|
|
62
|
+
Continue()
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def send_email_confirmation(user:, **)
|
|
66
|
+
UserMailer.with(
|
|
67
|
+
user:,
|
|
68
|
+
token: user.generate_token_for(:email_confirmation)
|
|
69
|
+
).email_confirmation.deliver_later
|
|
70
|
+
|
|
71
|
+
Continue()
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<small>
|
|
2
|
+
|
|
3
|
+
`Previous` [Intermediate Usage](./030_INTERMEDIATE_USAGE.md) | `Next` [Error Handling](./050_ERROR_HANDLING.md)
|
|
4
|
+
|
|
5
|
+
</small>
|
|
6
|
+
|
|
7
|
+
# Advanced Usage
|
|
8
|
+
|
|
9
|
+
**Status:** 🟡 `in-progress`
|
|
10
|
+
|
|
11
|
+
In this section, we will learn how to use input normalization and validation, dependencies, and nested processes.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
class User::Registration < Solid::Process
|
|
15
|
+
deps do
|
|
16
|
+
attribute :mailer, default: UserMailer
|
|
17
|
+
attribute :token_creation, default: User::Token::Creation
|
|
18
|
+
attribute :task_list_creation, default: Account::Task::List::Creation
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
input do
|
|
22
|
+
attribute :email, :string
|
|
23
|
+
attribute :password, :string
|
|
24
|
+
attribute :password_confirmation, :string
|
|
25
|
+
|
|
26
|
+
before_validation do
|
|
27
|
+
self.email = email.downcase.strip
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
with_options presence: true do
|
|
31
|
+
validates :email, format: User::Email::REGEXP
|
|
32
|
+
validates :password, confirmation: true, length: {minimum: User::Password::MINIMUM_LENGTH}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(attributes)
|
|
37
|
+
rollback_on_failure {
|
|
38
|
+
Given(attributes)
|
|
39
|
+
.and_then(:check_if_email_is_taken)
|
|
40
|
+
.and_then(:create_user)
|
|
41
|
+
.and_then(:create_user_account)
|
|
42
|
+
.and_then(:create_user_inbox)
|
|
43
|
+
.and_then(:create_user_token)
|
|
44
|
+
}
|
|
45
|
+
.and_then(:send_email_confirmation)
|
|
46
|
+
.and_expose(:user_registered, [:user])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def check_if_email_is_taken(email:, **)
|
|
52
|
+
input.errors.add(:email, "has already been taken") if User.exists?(email:)
|
|
53
|
+
|
|
54
|
+
input.errors.any? ? Failure(:invalid_input, input:) : Continue()
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def create_user(email:, password:, password_confirmation:, **)
|
|
58
|
+
user = User.create(email:, password:, password_confirmation:)
|
|
59
|
+
|
|
60
|
+
return Continue(user:) if user.persisted?
|
|
61
|
+
|
|
62
|
+
input.errors.merge!(user.errors)
|
|
63
|
+
|
|
64
|
+
Failure(:invalid_input, input:)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_user_account(user:, **)
|
|
68
|
+
account = Account.create!(uuid: SecureRandom.uuid)
|
|
69
|
+
|
|
70
|
+
account.memberships.create!(user:, role: :owner)
|
|
71
|
+
|
|
72
|
+
Continue(account:)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def create_user_inbox(account:, **)
|
|
76
|
+
case deps.task_list_creation.call(account:, inbox: true)
|
|
77
|
+
in Solid::Success(task_list:) then Continue()
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def create_user_token(user:, **)
|
|
82
|
+
case deps.token_creation.call(user:)
|
|
83
|
+
in Solid::Success(token:) then Continue()
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def send_email_confirmation(user:, **)
|
|
88
|
+
deps.mailer.with(
|
|
89
|
+
user:,
|
|
90
|
+
token: user.generate_token_for(:email_confirmation)
|
|
91
|
+
).email_confirmation.deliver_later
|
|
92
|
+
|
|
93
|
+
Continue()
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
data/docs/060_TESTING.md
ADDED
data/lib/solid/input.rb
CHANGED
data/lib/solid/model.rb
CHANGED
|
@@ -5,6 +5,16 @@ require_relative "model/access"
|
|
|
5
5
|
module Solid::Model
|
|
6
6
|
extend ::ActiveSupport::Concern
|
|
7
7
|
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def [](...)
|
|
10
|
+
new(...)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def inherited(subclass)
|
|
14
|
+
subclass.include(::Solid::Model)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
8
18
|
included do
|
|
9
19
|
include ::ActiveModel.const_defined?(:Api, false) ? ::ActiveModel::Api : ::ActiveModel::Model
|
|
10
20
|
include ::ActiveModel.const_defined?(:Access, false) ? ::ActiveModel::Access : ::Solid::Model::Access
|
|
@@ -12,19 +22,21 @@ module Solid::Model
|
|
|
12
22
|
include ::ActiveModel::Attributes
|
|
13
23
|
include ::ActiveModel::Dirty
|
|
14
24
|
include ::ActiveModel::Validations::Callbacks
|
|
25
|
+
|
|
26
|
+
extend ActiveModel::Callbacks
|
|
27
|
+
|
|
28
|
+
define_model_callbacks :initialize, only: :after
|
|
15
29
|
end
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
new(...)
|
|
20
|
-
end
|
|
31
|
+
def initialize(...)
|
|
32
|
+
super
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
subclass.include(::Solid::Model)
|
|
24
|
-
end
|
|
34
|
+
run_callbacks(:initialize)
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
def inspect
|
|
28
38
|
"#<#{self.class.name} #{attributes.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}>"
|
|
29
39
|
end
|
|
40
|
+
|
|
41
|
+
alias_method :[], :public_send
|
|
30
42
|
end
|
data/lib/solid/output.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Solid::Process::BacktraceCleaner < ActiveSupport::BacktraceCleaner
|
|
4
|
+
def initialize
|
|
5
|
+
super
|
|
6
|
+
|
|
7
|
+
add_blocks_silencer
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
BLOCKS_PATTERN = /in [`']block in|in [`'](?:Kernel#)?then'|internal:kernel|block \(\d+ levels?\) in/.freeze
|
|
13
|
+
|
|
14
|
+
def add_blocks_silencer
|
|
15
|
+
add_silencer { |line| line.match?(BLOCKS_PATTERN) }
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/solid/process/caller.rb
CHANGED
|
@@ -16,7 +16,7 @@ class Solid::Process
|
|
|
16
16
|
elsif input.invalid?
|
|
17
17
|
Failure(:invalid_input, input: input)
|
|
18
18
|
else
|
|
19
|
-
super(input.attributes.
|
|
19
|
+
super(input.attributes.symbolize_keys)
|
|
20
20
|
end
|
|
21
21
|
rescue ::Exception => exception
|
|
22
22
|
rescue_with_handler(exception) || raise
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Solid::Process::EventLogs::BasicLoggerListener
|
|
4
|
+
include Solid::Result::EventLogs::Listener
|
|
5
|
+
|
|
6
|
+
class_attribute :logger, :backtrace_cleaner
|
|
7
|
+
|
|
8
|
+
self.logger = ActiveSupport::Logger.new($stdout)
|
|
9
|
+
self.backtrace_cleaner = Solid::Process::BacktraceCleaner.new
|
|
10
|
+
|
|
11
|
+
module MessagesNesting
|
|
12
|
+
MAP_STEP_METHOD = lambda do |record_result|
|
|
13
|
+
kind, type, value = record_result.values_at(:kind, :type, :value)
|
|
14
|
+
|
|
15
|
+
value_keys = "#{value.keys.join(":, ")}:"
|
|
16
|
+
value_keys = "" if value_keys == ":"
|
|
17
|
+
|
|
18
|
+
case type
|
|
19
|
+
when :_given_ then "Given(#{value_keys})"
|
|
20
|
+
when :_continue_ then "Continue(#{value_keys})"
|
|
21
|
+
else "#{kind.capitalize}(:#{type}, #{value_keys})"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
MAP_STEP_MESSAGE = lambda do |record|
|
|
26
|
+
step = MAP_STEP_METHOD[record[:result]]
|
|
27
|
+
|
|
28
|
+
method_name = record.dig(:and_then, :method_name)
|
|
29
|
+
|
|
30
|
+
" * #{step} from method: #{method_name}".chomp("from method: ").chomp(" ")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
MAP_IDS_WITH_MESSAGES = lambda do |records|
|
|
34
|
+
process_ids = []
|
|
35
|
+
|
|
36
|
+
records.each_with_object([]) do |record, messages|
|
|
37
|
+
current = record[:current]
|
|
38
|
+
|
|
39
|
+
current_id = current[:id]
|
|
40
|
+
|
|
41
|
+
unless process_ids.include?(current_id)
|
|
42
|
+
process_ids << current_id
|
|
43
|
+
|
|
44
|
+
id, name, desc = current.values_at(:id, :name, :desc)
|
|
45
|
+
|
|
46
|
+
messages << [current_id, "##{id} #{name} - #{desc}".chomp("- ").chomp(" ")]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
messages << [current_id, MAP_STEP_MESSAGE[record]]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.map(event_logs)
|
|
54
|
+
ids_level_parent = event_logs.dig(:metadata, :ids, :level_parent)
|
|
55
|
+
|
|
56
|
+
messages = MAP_IDS_WITH_MESSAGES[event_logs[:records]]
|
|
57
|
+
|
|
58
|
+
messages.map { |(id, msg)| "#{" " * ids_level_parent[id].first}#{msg}" }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_finish(event_logs:)
|
|
63
|
+
messages = MessagesNesting.map(event_logs)
|
|
64
|
+
|
|
65
|
+
logger.info messages.join("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def before_interruption(exception:, event_logs:)
|
|
69
|
+
messages = MessagesNesting.map(event_logs)
|
|
70
|
+
|
|
71
|
+
logger.info messages.join("\n")
|
|
72
|
+
|
|
73
|
+
cleaned_backtrace = backtrace_cleaner.clean(exception.backtrace).join("\n ")
|
|
74
|
+
|
|
75
|
+
logger.error "\nException:\n #{exception.message} (#{exception.class})\n\nBacktrace:\n #{cleaned_backtrace}"
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/solid/process.rb
CHANGED
|
@@ -2,18 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_support/all"
|
|
4
4
|
require "active_model"
|
|
5
|
-
require "solid/result"
|
|
6
5
|
|
|
7
6
|
module Solid
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def self.Failure(...)
|
|
15
|
-
::Solid::Output::Failure(...)
|
|
16
|
-
end
|
|
7
|
+
require_relative "model"
|
|
8
|
+
require_relative "value"
|
|
9
|
+
require_relative "input"
|
|
10
|
+
require_relative "output"
|
|
17
11
|
|
|
18
12
|
class Process
|
|
19
13
|
require "solid/process/version"
|
|
@@ -23,6 +17,8 @@ module Solid
|
|
|
23
17
|
require "solid/process/callbacks"
|
|
24
18
|
require "solid/process/class_methods"
|
|
25
19
|
require "solid/process/active_record"
|
|
20
|
+
require "solid/process/backtrace_cleaner"
|
|
21
|
+
require "solid/process/event_logs"
|
|
26
22
|
|
|
27
23
|
extend ClassMethods
|
|
28
24
|
|
|
@@ -40,15 +36,17 @@ module Solid
|
|
|
40
36
|
new.call(arg)
|
|
41
37
|
end
|
|
42
38
|
|
|
43
|
-
def self.
|
|
39
|
+
def self.config
|
|
40
|
+
Config.instance
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.configuration(freeze: true, &block)
|
|
44
44
|
yield config
|
|
45
45
|
|
|
46
|
-
config.freeze
|
|
46
|
+
config.tap { _1.freeze if freeze }
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
Config.instance
|
|
51
|
-
end
|
|
49
|
+
singleton_class.alias_method :configure, :configuration
|
|
52
50
|
|
|
53
51
|
attr_reader :output, :input, :dependencies
|
|
54
52
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class EmailValidator < ActiveModel::EachValidator
|
|
4
|
-
def validate_each(
|
|
4
|
+
def validate_each(model, attribute, value)
|
|
5
|
+
return model.errors.add(attribute, :blank, **options) if value.blank?
|
|
6
|
+
|
|
5
7
|
return if value.is_a?(String) && URI::MailTo::EMAIL_REGEXP.match?(value)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
model.errors.add(attribute, :invalid, **options)
|
|
8
10
|
end
|
|
9
11
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "solid/validators"
|
|
4
|
+
|
|
5
|
+
class IdValidator < ActiveModel::EachValidator
|
|
6
|
+
OPTIONS = {only_integer: true, greater_than: 0}.freeze
|
|
7
|
+
|
|
8
|
+
def validate_each(model, attribute, value)
|
|
9
|
+
opts = OPTIONS.merge(options.except(*OPTIONS.keys))
|
|
10
|
+
|
|
11
|
+
opts[:attributes] = attribute
|
|
12
|
+
|
|
13
|
+
::ActiveModel::Validations::NumericalityValidator.new(opts).validate_each(model, attribute, value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private_constant :OPTIONS
|
|
17
|
+
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "solid/validators"
|
|
4
|
+
|
|
3
5
|
class InstanceOfValidator < ActiveModel::EachValidator
|
|
4
|
-
def validate_each(
|
|
6
|
+
def validate_each(model, attribute, value)
|
|
5
7
|
with_option = Array.wrap(options[:with] || options[:in])
|
|
6
8
|
|
|
7
9
|
return if with_option.any? { |type| value.instance_of?(type) }
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
message = "is not an instance of #{with_option.map(&:name).join(" | ")}"
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Solid::Validators.add_error(model, attribute, message, options)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "solid/validators"
|
|
4
|
+
|
|
5
|
+
class IsValidator < ActiveModel::EachValidator
|
|
6
|
+
def validate_each(model, attribute, value)
|
|
7
|
+
with_option = Array.wrap(options[:with] || options[:in])
|
|
8
|
+
|
|
9
|
+
return if with_option.all? do |predicate|
|
|
10
|
+
raise ArgumentError, "expected a predicate method, got #{predicate.inspect}" unless predicate.end_with?("?")
|
|
11
|
+
|
|
12
|
+
value.try(predicate)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
message = "does not satisfy the predicate#{"s" if with_option.size > 1}: #{with_option.join(" & ")}"
|
|
16
|
+
|
|
17
|
+
Solid::Validators.add_error(model, attribute, message, options)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "solid/validators"
|
|
4
|
+
|
|
3
5
|
class KindOfValidator < ActiveModel::EachValidator
|
|
4
|
-
def validate_each(
|
|
6
|
+
def validate_each(model, attribute, value)
|
|
5
7
|
with_option = Array.wrap(options[:with] || options[:in])
|
|
6
8
|
|
|
7
9
|
return if with_option.any? { |type| value.is_a?(type) }
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
message = "is not a #{with_option.map(&:name).join(" | ")}"
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Solid::Validators.add_error(model, attribute, message, options)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "solid/validators"
|
|
4
|
+
|
|
3
5
|
class RespondToValidator < ActiveModel::EachValidator
|
|
4
|
-
def validate_each(
|
|
6
|
+
def validate_each(model, attribute, value)
|
|
5
7
|
with_option = Array.wrap(options[:with] || options[:in])
|
|
6
8
|
|
|
7
9
|
return if with_option.all? { value.respond_to?(_1) }
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
message = "does not respond to #{with_option.map(&:inspect).join(" & ")}"
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Solid::Validators.add_error(model, attribute, message, options)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "solid/validators"
|
|
4
|
+
|
|
3
5
|
class SingletonValidator < ActiveModel::EachValidator
|
|
4
|
-
def validate_each(
|
|
6
|
+
def validate_each(model, attribute, value)
|
|
5
7
|
with_option = Array.wrap(options[:with] || options[:in])
|
|
6
8
|
|
|
7
|
-
unless value.is_a?(Module)
|
|
8
|
-
return obj.errors.add(attribute, options[:message] || "is not a class or module")
|
|
9
|
-
end
|
|
9
|
+
return model.errors.add(attribute, options[:message] || "is not a class or module") unless value.is_a?(Module)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
return if with_option.any? do |type|
|
|
12
|
+
raise ArgumentError, "#{type.inspect} is not a class or module" unless type.is_a?(Module)
|
|
13
13
|
|
|
14
14
|
value == type || (value < type || value.is_a?(type))
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
message = "is not #{with_option.map(&:name).join(" | ")}"
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Solid::Validators.add_error(model, attribute, message, options)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -5,16 +5,16 @@ class UuidValidator < ActiveModel::EachValidator
|
|
|
5
5
|
CASE_SENSITIVE = /\A#{PATTERN}\z/.freeze
|
|
6
6
|
CASE_INSENSITIVE = /\A#{PATTERN}\z/i.freeze
|
|
7
7
|
|
|
8
|
-
def validate_each(
|
|
8
|
+
def validate_each(model, attribute, value)
|
|
9
9
|
case_sensitive = options.fetch(:case_sensitive, true)
|
|
10
10
|
|
|
11
|
+
return model.errors.add(attribute, :blank, **options) if value.blank?
|
|
12
|
+
|
|
11
13
|
regexp = case_sensitive ? CASE_SENSITIVE : CASE_INSENSITIVE
|
|
12
14
|
|
|
13
15
|
return if value.is_a?(String) && value.match?(regexp)
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
obj.errors.add(attribute, message)
|
|
17
|
+
model.errors.add(attribute, :invalid, **options)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
private_constant :PATTERN
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solid
|
|
4
|
+
module Validators
|
|
5
|
+
def self.add_error(model, attribute, message, options)
|
|
6
|
+
if ActiveModel.const_defined?(:Error)
|
|
7
|
+
model.errors.add(attribute, **options.merge(message: message))
|
|
8
|
+
else
|
|
9
|
+
model.errors.add(attribute, options.fetch(:message, message))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/solid/value.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solid::Value
|
|
4
|
+
module ClassMethods
|
|
5
|
+
UNDEFINED = ::Object.new
|
|
6
|
+
|
|
7
|
+
def new(value = UNDEFINED)
|
|
8
|
+
return value if value.is_a?(self)
|
|
9
|
+
|
|
10
|
+
UNDEFINED.equal?(value) ? super() : super(value: value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def attribute(...)
|
|
14
|
+
super(:value, ...)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validates(...)
|
|
18
|
+
super(:value, ...)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.included(subclass)
|
|
23
|
+
subclass.include Solid::Model
|
|
24
|
+
subclass.extend ClassMethods
|
|
25
|
+
subclass.attribute
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ==(other)
|
|
29
|
+
other.is_a?(self.class) && other.value == value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def hash
|
|
33
|
+
value.hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
value.to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias_method :eql?, :==
|
|
41
|
+
end
|