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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/CLAUDE.md +112 -0
  3. data/CHANGELOG.md +92 -1
  4. data/README.md +208 -23
  5. data/Rakefile +28 -8
  6. data/docs/010_KEY_CONCEPTS.md +31 -0
  7. data/docs/020_BASIC_USAGE.md +46 -0
  8. data/docs/030_INTERMEDIATE_USAGE.md +74 -0
  9. data/docs/040_ADVANCED_USAGE.md +96 -0
  10. data/docs/050_ERROR_HANDLING.md +3 -0
  11. data/docs/060_TESTING.md +3 -0
  12. data/docs/070_INSTRUMENTATION.md +3 -0
  13. data/docs/080_RAILS_INTEGRATION.md +3 -0
  14. data/docs/090_INTERNAL_LIBRARIES.md +3 -0
  15. data/docs/100_PORTS_AND_ADAPTERS.md +3 -0
  16. data/lib/solid/input.rb +0 -2
  17. data/lib/solid/model.rb +19 -7
  18. data/lib/solid/output.rb +13 -0
  19. data/lib/solid/process/backtrace_cleaner.rb +17 -0
  20. data/lib/solid/process/caller.rb +1 -1
  21. data/lib/solid/process/event_logs/basic_logger_listener.rb +77 -0
  22. data/lib/solid/process/event_logs.rb +7 -0
  23. data/lib/solid/process/version.rb +1 -1
  24. data/lib/solid/process.rb +13 -15
  25. data/lib/solid/validators/email_validator.rb +4 -2
  26. data/lib/solid/validators/id_validator.rb +17 -0
  27. data/lib/solid/validators/instance_of_validator.rb +5 -3
  28. data/lib/solid/validators/is_validator.rb +19 -0
  29. data/lib/solid/validators/kind_of_validator.rb +5 -3
  30. data/lib/solid/validators/respond_to_validator.rb +5 -3
  31. data/lib/solid/validators/singleton_validator.rb +8 -8
  32. data/lib/solid/validators/uuid_validator.rb +4 -4
  33. data/lib/solid/validators.rb +13 -0
  34. data/lib/solid/value.rb +41 -0
  35. metadata +26 -16
  36. data/lib/solid/validators/all.rb +0 -12
  37. data/lib/solid/validators/bool_validator.rb +0 -9
  38. data/lib/solid/validators/is_a_validator.rb +0 -6
  39. 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
+ ```
@@ -0,0 +1,3 @@
1
+ # Error handling
2
+
3
+ **Status:** 🔴 `to-do`
@@ -0,0 +1,3 @@
1
+ # Testing
2
+
3
+ **Status:** 🔴 `to-do`
@@ -0,0 +1,3 @@
1
+ # Instrumentation
2
+
3
+ **Status:** 🔴 `to-do`
@@ -0,0 +1,3 @@
1
+ # Instrumentation
2
+
3
+ **Status:** 🔴 `to-do`
@@ -0,0 +1,3 @@
1
+ # Internal Libraries
2
+
3
+ **Status:** 🔴 `to-do`
@@ -0,0 +1,3 @@
1
+ # Ports and Adapters (Hexagonal Architecture)
2
+
3
+ **Status:** 🔴 `to-do`
data/lib/solid/input.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "model"
4
-
5
3
  class Solid::Input
6
4
  include ::Solid::Model
7
5
  end
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
- module ClassMethods
18
- def [](...)
19
- new(...)
20
- end
31
+ def initialize(...)
32
+ super
21
33
 
22
- def inherited(subclass)
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solid/result"
4
+
5
+ module Solid
6
+ def self.Success(...)
7
+ ::Solid::Output::Success(...)
8
+ end
9
+
10
+ def self.Failure(...)
11
+ ::Solid::Output::Failure(...)
12
+ end
13
+ end
@@ -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
@@ -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.deep_symbolize_keys)
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Solid::Process
4
+ module EventLogs
5
+ require_relative "event_logs/basic_logger_listener"
6
+ end
7
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Solid
4
4
  class Process
5
- VERSION = "0.3.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  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
- require "solid/input"
9
-
10
- def self.Success(...)
11
- ::Solid::Output::Success(...)
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.configuration(&block)
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
- def self.config
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(obj, attribute, value)
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
- obj.errors.add attribute, (options[:message] || "is not an email")
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(obj, attribute, value)
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
- expectation = with_option.map(&:name).join(" | ")
11
+ message = "is not an instance of #{with_option.map(&:name).join(" | ")}"
10
12
 
11
- obj.errors.add(attribute, (options[:message] || "is not an instance of #{expectation}"))
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(obj, attribute, value)
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
- expectation = with_option.map(&:name).join(" | ")
11
+ message = "is not a #{with_option.map(&:name).join(" | ")}"
10
12
 
11
- obj.errors.add(attribute, (options[:message] || "is not a #{expectation}"))
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(obj, attribute, value)
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
- expectation = with_option.map(&:inspect).join(" & ")
11
+ message = "does not respond to #{with_option.map(&:inspect).join(" & ")}"
10
12
 
11
- obj.errors.add(attribute, (options[:message] || "does not respond to #{expectation}"))
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(obj, attribute, value)
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
- is_valid = with_option.any? do |type|
12
- type.is_a?(Module) or raise ArgumentError, "#{type.inspect} is not a class or module"
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
- expectation = with_option.map(&:name).join(" | ")
17
+ message = "is not #{with_option.map(&:name).join(" | ")}"
18
18
 
19
- is_valid or obj.errors.add(attribute, (options[:message] || "is not #{expectation}"))
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(obj, attribute, value)
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
- message = options[:message] || "is not a valid UUID (case #{case_sensitive ? "sensitive" : "insensitive"})"
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
@@ -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