estate 0.0.2 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f445ae55c16632d7805cd1774f2800eed57ebf16ce63db5ef7e780225ecc52a1
4
- data.tar.gz: 60527e77ed67584c663bde4504a104c1b9b38e179fdd299e5344bef2031d6bb9
3
+ metadata.gz: a5e27471f690e289d113e8e69a6c72ad1c063a20b57bdead1f97040b71cb101f
4
+ data.tar.gz: 233b56c93ade0cc71d91d721bd58f207bc17bc320b9e2165d31ad2a4700786d7
5
5
  SHA512:
6
- metadata.gz: e27a18f1f5a82b3b2316d559eb7847f400f15b025fad62dc3d1ced8106856d2c8c60014a86af2837e81f03cf6956175af7f2ab0dabf08d0b0d4fb6b7dac37495
7
- data.tar.gz: a2f52b92fff0482abbc7981ab5685d59b5430e171f37c568f68f8ab72d1ed57d851886b0bbf631c472ee34ced52699203571c1ffd420ee636d94cfa00daf1984
6
+ metadata.gz: c1330c6a3512171bdb6deaa89612fa4adf4d6aad6ef621e97aa456c0e792e48f21b1d7cac9ef47fe36a7a8b1806c6148da9b973200e1ff5f27e5434deac43c11
7
+ data.tar.gz: 1452e5b9408d8cfd658ed983d11f1f8a0300e533400875285af65c84ee7a451edf6f29be6df4f779e991ce47a25fd6e230404b8949d37a15a1353414ef1be521
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Estate Gem
2
2
 
3
- Estate is a Ruby gem designed to simplify state management in ActiveRecord models. The primary focus of this gem is to provide a straightforward way to define states and transitions using a clean syntax, allowing seamless integration into ActiveRecord models.
3
+ Estate is a Ruby gem designed to simplify state management in both ActiveRecord and Sequel models. The primary focus of this gem is to provide a straightforward way to define states and transitions using a clean syntax.
4
4
 
5
5
  ## Installation
6
6
 
@@ -17,8 +17,8 @@ gem install estate
17
17
  ```
18
18
 
19
19
  ## Usage
20
-
21
- To use the Estate gem, include it in your ActiveRecord model and define your states and transitions inside a block using the `estate` method. Here's a simple example:
20
+ ### ActiveRecord
21
+ To use the Estate gem with ActiveRecord, include it in your model and define your states and transitions inside a block using the `estate` method. Here's a simple example:
22
22
 
23
23
  ```ruby
24
24
  class MyModel < ApplicationRecord
@@ -36,13 +36,59 @@ class MyModel < ApplicationRecord
36
36
  end
37
37
  ```
38
38
 
39
- The default field for storing the state is named state. You can customize this field by providing options to the estate method:
39
+ And then
40
+
41
+ ```ruby
42
+ model = MyModel.create(state: :state_1)
43
+ model.update(state: :state_2) # you don't need to call any extra code to change the state; treat it like a normal field
44
+ ```
45
+
46
+ The default field for storing the state is named "state". You can customize this name by providing options to the estate method:
40
47
 
41
48
  ```ruby
42
49
  class MyModel < ApplicationRecord
43
50
  include Estate
44
51
 
45
- estate(column: :custom_state_field, empty_initial_state: true) do
52
+ estate column: :custom_state_field do
53
+ # ...
54
+ end
55
+ end
56
+ ```
57
+
58
+ You can also use the `empty_initial_state: true` option to enable the creation of a model with a `nil` initial state:
59
+
60
+ ```ruby
61
+ class MyModel < ApplicationRecord
62
+ include Estate
63
+
64
+ estate empty_initial_state: true do
65
+ # ...
66
+ end
67
+ end
68
+ ```
69
+
70
+ The `estate` method now supports a `raise_on_error` option. When set to `true`, the gem will raise a specific exception instead of the standard ActiveRecord validation error upon a validation failure.
71
+
72
+ ```ruby
73
+ class MyModel < ApplicationRecord
74
+ include Estate
75
+
76
+ estate raise_on_error: true do
77
+ # ...
78
+ end
79
+ end
80
+ ```
81
+
82
+ ### Sequel
83
+ To use the Estate gem with Sequel, include it in your model, and ensure you have the `plugin: dirty` enabled for validation to work correctly. The `raise_on_error` option is not needed with Sequel, as exceptions are always raised on validation errors.
84
+
85
+ ```ruby
86
+ class MySequelModel < Sequel::Model
87
+ include Estate
88
+
89
+ plugin :dirty # Ensure the dirty plugin is enabled for validation to work
90
+
91
+ estate do
46
92
  state :state_1
47
93
  state :state_2
48
94
  state :state_3
@@ -54,7 +100,7 @@ class MyModel < ApplicationRecord
54
100
  end
55
101
  ```
56
102
 
57
- ## Migration Example
103
+ ## Migration Example for ActiveRecord
58
104
 
59
105
  ```bash
60
106
  bundle exec rails generate migration AddStateToMyModels state:string
@@ -3,17 +3,19 @@
3
3
  module Estate
4
4
  module Configuration
5
5
  class << self
6
- def init_config(column_name:, allow_empty_initial_state:)
6
+ def init_config(column_name:, allow_empty_initial_state:, raise_on_error:)
7
7
  @column_name = column_name
8
8
  @allow_empty_initial_state = allow_empty_initial_state
9
+ @raise_on_error = raise_on_error
9
10
  end
10
11
 
11
- attr_reader :column_name, :allow_empty_initial_state
12
+ attr_reader :column_name, :allow_empty_initial_state, :raise_on_error
12
13
  end
13
14
 
14
15
  module Defaults
15
16
  COLUMN_NAME = :state
16
17
  ALLOW_EMPTY_INITIAL_STATE = false
18
+ RAISE_ON_ERROR = false
17
19
  end
18
20
  end
19
21
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Core
5
+ module_function
6
+
7
+ def setup(base)
8
+ if 'ActiveRecord::Base'.in? base.ancestors.map(&:to_s)
9
+ require File.join(File.dirname(__FILE__), 'db', 'active_record')
10
+ Estate::Db::ActiveRecord.setup_callbacks(base)
11
+ else
12
+ require File.join(File.dirname(__FILE__), 'db', 'sequel')
13
+ Estate::Db::Sequel.setup_callbacks(base)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Db
5
+ module ActiveRecord
6
+ module_function
7
+
8
+ def setup_callbacks(base)
9
+ base.class_eval do
10
+ public_send(:before_validation) do
11
+ from_state = public_send("#{Estate::Configuration.column_name}_was")
12
+ to_state = public_send(Estate::Configuration.column_name)
13
+ Estate::Db::ActiveRecord.validate_state_changes(self, from_state, to_state)
14
+ end
15
+ end
16
+ end
17
+
18
+ def validate_state_changes(instance, from_state, to_state)
19
+ if from_state == to_state
20
+ if from_state.nil? && !Estate::Configuration.allow_empty_initial_state
21
+ add_error(instance: instance, message: "empty `#{Estate::Configuration.column_name}` is not allowed")
22
+ end
23
+ elsif to_state.nil?
24
+ add_error(instance: instance, message: 'transition to empty state is not allowed')
25
+ elsif !Estate::StateMachine.state_exists?(to_state)
26
+ add_error(instance: instance, message: "state `#{to_state}` is not defined")
27
+ elsif !transition_allowed?(from_state: from_state, to_state: to_state)
28
+ add_error(instance: instance, message: "transition from `#{from_state}` to `#{to_state}` is not allowed",
29
+ attribute: Estate::Configuration.column_name)
30
+ end
31
+ end
32
+
33
+ def add_error(instance:, message:, attribute: :base)
34
+ if Estate::Configuration.raise_on_error
35
+ exception_message = attribute == :base ? message : "#{attribute}: #{message}"
36
+ raise(StandardError, exception_message)
37
+ else
38
+ instance.errors.add(attribute, message) unless instance.errors[attribute].include?(message)
39
+ end
40
+ end
41
+
42
+ def transition_allowed?(from_state:, to_state:)
43
+ from_state.nil? || Estate::StateMachine.transition_exists?(from: from_state, to: to_state)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Db
5
+ module Sequel
6
+ module_function
7
+
8
+ def setup_callbacks(base)
9
+ base.class_eval do
10
+ def validate
11
+ super
12
+
13
+ to_state = values[Estate::Configuration.column_name]
14
+ from_state, = column_change(Estate::Configuration.column_name)
15
+ Estate::Db::Sequel.validate_state_changes(self, from_state, to_state)
16
+ end
17
+ end
18
+ end
19
+
20
+ def validate_state_changes(instance, from_state, to_state)
21
+ if from_state == to_state
22
+ if from_state.nil? && !Estate::Configuration.allow_empty_initial_state
23
+ add_error(instance: instance, message: "empty `#{Estate::Configuration.column_name}` is not allowed")
24
+ end
25
+ elsif to_state.nil?
26
+ add_error(instance: instance, message: 'transition to empty state is not allowed')
27
+ elsif !Estate::StateMachine.state_exists?(to_state)
28
+ add_error(instance: instance, message: "state `#{to_state}` is not defined")
29
+ elsif !transition_allowed?(from_state: from_state, to_state: to_state)
30
+ add_error(instance: instance, message: "transition from `#{from_state}` to `#{to_state}` is not allowed",
31
+ attribute: Estate::Configuration.column_name)
32
+ end
33
+ end
34
+
35
+ # TODO: remove base
36
+ def add_error(instance:, message:, attribute: :base)
37
+ instance.errors.add(attribute, message)
38
+ end
39
+
40
+ def transition_allowed?(from_state:, to_state:)
41
+ from_state.nil? || Estate::StateMachine.transition_exists?(from: from_state, to: to_state)
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/estate/estate.rb CHANGED
@@ -5,16 +5,16 @@ module Estate
5
5
  base.extend Estate::ClassMethods
6
6
 
7
7
  Estate::Requirements.check_requirements(base)
8
- Estate::ActiveRecord.setup_callbacks(base)
9
8
  Estate::StateMachine.create_store
10
-
11
- super
9
+ Estate::Core.setup(base)
12
10
  end
13
11
 
14
12
  module ClassMethods
15
13
  def estate(column: Estate::Configuration::Defaults::COLUMN_NAME,
16
- empty_initial_state: Estate::Configuration::Defaults::ALLOW_EMPTY_INITIAL_STATE)
17
- Estate::Configuration.init_config(column_name: column, allow_empty_initial_state: empty_initial_state)
14
+ empty_initial_state: Estate::Configuration::Defaults::ALLOW_EMPTY_INITIAL_STATE,
15
+ raise_on_error: Estate::Configuration::Defaults::RAISE_ON_ERROR)
16
+ Estate::Configuration.init_config(column_name: column, allow_empty_initial_state: empty_initial_state,
17
+ raise_on_error: raise_on_error)
18
18
 
19
19
  yield if block_given?
20
20
  end
@@ -5,7 +5,9 @@ module Estate
5
5
  def check_requirements(base)
6
6
  ancestors = base.ancestors.map(&:to_s)
7
7
 
8
- raise(StandardError, 'Estate requires ActiveRecord') unless ancestors.include?('ActiveRecord::Base')
8
+ unless 'Sequel::Model'.in?(ancestors) || 'ActiveRecord::Base'.in?(ancestors)
9
+ raise(StandardError, 'Estate requires ActiveRecord or Sequel')
10
+ end
9
11
  end
10
12
 
11
13
  module_function :check_requirements
@@ -24,7 +24,6 @@ module Estate
24
24
  end
25
25
 
26
26
  def transition_exists?(from:, to:)
27
- # TODO: validate from and to
28
27
  transition_key = { from: from.to_sym, to: to.to_sym }
29
28
  transitions.key?(transition_key)
30
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Estate
4
- VERSION = '0.0.2'
4
+ VERSION = '0.1.0'
5
5
  end
data/lib/estate.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'estate/version'
4
- require 'estate/requirements'
5
3
  require 'estate/configuration'
6
- require 'estate/state_machine'
7
- require 'estate/active_record'
4
+ require 'estate/core'
8
5
  require 'estate/estate'
6
+ require 'estate/requirements'
7
+ require 'estate/state_machine'
8
+ require 'estate/version'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: estate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Korepanov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-22 00:00:00.000000000 Z
11
+ date: 2024-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - '='
67
67
  - !ruby/object:Gem::Version
68
68
  version: 3.12.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.22.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.22.0
69
83
  description: Estate is a Ruby gem designed to simplify state management in ActiveRecord
70
84
  models
71
85
  email: noemail@example.com
@@ -76,8 +90,10 @@ files:
76
90
  - LICENSE.txt
77
91
  - README.md
78
92
  - lib/estate.rb
79
- - lib/estate/active_record.rb
80
93
  - lib/estate/configuration.rb
94
+ - lib/estate/core.rb
95
+ - lib/estate/db/active_record.rb
96
+ - lib/estate/db/sequel.rb
81
97
  - lib/estate/estate.rb
82
98
  - lib/estate/requirements.rb
83
99
  - lib/estate/state_machine.rb
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Estate
4
- module ActiveRecord
5
- CALLBACK_NAMES = [:before_validation].freeze
6
-
7
- def setup_callbacks(base)
8
- base.class_eval do
9
- CALLBACK_NAMES.each do |callback_name|
10
- public_send(callback_name) { Estate::ActiveRecord.validate_state_changes(self) }
11
- end
12
- end
13
- end
14
-
15
- def validate_state_changes(instance)
16
- from_state = instance.public_send("#{Estate::Configuration.column_name}_was")
17
- to_state = instance.public_send(Estate::Configuration.column_name)
18
-
19
- if from_state == to_state
20
- if to_state.nil? && !Estate::Configuration.allow_empty_initial_state
21
- raise(StandardError, "empty `#{Estate::Configuration.column_name}` is not allowed")
22
- end
23
- elsif Estate::StateMachine.state_exists?(to_state) # TODO: check to_state.nil?
24
- unless Estate::StateMachine.transition_exists?(from: from_state, to: to_state)
25
- instance.errors.add(Estate::Configuration.column_name,
26
- message: "transition from `#{from_state}` to `#{to_state}` is not allowed")
27
- end
28
- else
29
- instance.errors.add(:base, "state `#{to_state}` is not defined")
30
- end
31
- end
32
-
33
- module_function :setup_callbacks, :validate_state_changes
34
- end
35
- end