estate 0.0.2 → 0.1.1

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: 0777265b6deae44f96eb4fbc8f150bb73b1f2d98849107b9750eeb0ac025ca49
4
+ data.tar.gz: 916c2a835c05ed793d87e25883cb6999ebf87687c260f39eba90e994cf0aa228
5
5
  SHA512:
6
- metadata.gz: e27a18f1f5a82b3b2316d559eb7847f400f15b025fad62dc3d1ced8106856d2c8c60014a86af2837e81f03cf6956175af7f2ab0dabf08d0b0d4fb6b7dac37495
7
- data.tar.gz: a2f52b92fff0482abbc7981ab5685d59b5430e171f37c568f68f8ab72d1ed57d851886b0bbf631c472ee34ced52699203571c1ffd420ee636d94cfa00daf1984
6
+ metadata.gz: 37e595f2f4451386df31a2b563af980037c90f91786760c9c88f9090418ffd3b640473bc5c2d16a1826c3646302d2041b151a0807d106e1dbc6c85dc126bbfbd
7
+ data.tar.gz: f939381283ace662a7d97169cc7e0bf26ad8d05a78eb40585f456e84768bda4474c1ae0501d5f7d232eb401d577341d2ce09f4fa3b02137adca8019e87c65114
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
+ ![CI](https://github.com/igorkorepanov/estate/actions/workflows/main.yml/badge.svg)
2
+
1
3
  # Estate Gem
2
4
 
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.
5
+ 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
6
 
5
7
  ## Installation
6
8
 
@@ -17,8 +19,8 @@ gem install estate
17
19
  ```
18
20
 
19
21
  ## 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:
22
+ ### ActiveRecord
23
+ 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
24
 
23
25
  ```ruby
24
26
  class MyModel < ApplicationRecord
@@ -36,13 +38,59 @@ class MyModel < ApplicationRecord
36
38
  end
37
39
  ```
38
40
 
39
- The default field for storing the state is named state. You can customize this field by providing options to the estate method:
41
+ And then
42
+
43
+ ```ruby
44
+ model = MyModel.create(state: :state_1)
45
+ model.update(state: :state_2) # you don't need to call any extra code to change the state; treat it like a normal field
46
+ ```
47
+
48
+ The default field for storing the state is named "state". You can customize this name by providing options to the estate method:
49
+
50
+ ```ruby
51
+ class MyModel < ApplicationRecord
52
+ include Estate
53
+
54
+ estate column: :custom_state_field do
55
+ # ...
56
+ end
57
+ end
58
+ ```
59
+
60
+ You can also use the `empty_initial_state: true` option to enable the creation of a model with a `nil` initial state:
61
+
62
+ ```ruby
63
+ class MyModel < ApplicationRecord
64
+ include Estate
65
+
66
+ estate empty_initial_state: true do
67
+ # ...
68
+ end
69
+ end
70
+ ```
71
+
72
+ 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.
40
73
 
41
74
  ```ruby
42
75
  class MyModel < ApplicationRecord
43
76
  include Estate
44
77
 
45
- estate(column: :custom_state_field, empty_initial_state: true) do
78
+ estate raise_on_error: true do
79
+ # ...
80
+ end
81
+ end
82
+ ```
83
+
84
+ ### Sequel
85
+ 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.
86
+
87
+ ```ruby
88
+ class MySequelModel < Sequel::Model
89
+ include Estate
90
+
91
+ plugin :dirty # Ensure the dirty plugin is enabled for validation to work
92
+
93
+ estate do
46
94
  state :state_1
47
95
  state :state_2
48
96
  state :state_3
@@ -54,7 +102,7 @@ class MyModel < ApplicationRecord
54
102
  end
55
103
  ```
56
104
 
57
- ## Migration Example
105
+ ## Migration Example for ActiveRecord
58
106
 
59
107
  ```bash
60
108
  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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Constants
5
+ module Orm
6
+ ACTIVE_RECORD = 'active_record'
7
+ SEQUEL = 'sequel'
8
+ end
9
+ end
10
+ end
data/lib/estate/estate.rb CHANGED
@@ -5,21 +5,22 @@ 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::Setup.call(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, empty_initial_state, raise_on_error)
18
17
 
19
18
  yield if block_given?
20
19
  end
21
20
 
22
- def state(name)
21
+ def state(name = nil)
22
+ raise(StandardError, 'state must be a Symbol or a String') unless Estate::StateMachine.argument_valid?(name)
23
+
23
24
  if Estate::StateMachine.state_exists?(name)
24
25
  raise(StandardError, "state `:#{name}` is already defined")
25
26
  else
@@ -27,16 +28,22 @@ module Estate
27
28
  end
28
29
  end
29
30
 
30
- def transition(from:, to:)
31
+ def transition(from: nil, to: nil)
32
+ unless Estate::StateMachine.argument_valid?(from)
33
+ raise(StandardError, 'argument `from` must be a Symbol or a String')
34
+ end
35
+
36
+ raise(StandardError, 'argument `to` must be a Symbol or a String') unless Estate::StateMachine.argument_valid?(to)
37
+
31
38
  raise(StandardError, "state `#{from}` is not defined") unless Estate::StateMachine.state_exists?(from)
32
39
 
33
40
  raise(StandardError, "state `#{to}` is not defined") unless Estate::StateMachine.state_exists?(to)
34
41
 
35
- if Estate::StateMachine.transition_exists?(from: from, to: to)
42
+ if Estate::StateMachine.transition_exists?(from, to)
36
43
  raise(StandardError, "`transition from: :#{from}, to: :#{to}` already defined")
37
44
  end
38
45
 
39
- Estate::StateMachine.register_transition(from: from, to: to)
46
+ Estate::StateMachine.register_transition(from, to)
40
47
  end
41
48
  end
42
49
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Logic
5
+ module ActiveRecord
6
+ module Setup
7
+ module_function
8
+
9
+ def call(base)
10
+ base.class_eval do
11
+ public_send(:before_validation) do
12
+ Estate::Logic::Core.call(Estate::Constants::Orm::ACTIVE_RECORD, self)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'estate/logic/common_logic'
4
+
5
+ module Estate
6
+ module Logic
7
+ module ActiveRecord
8
+ module SpecificLogic
9
+ extend Estate::Logic::CommonLogic
10
+
11
+ module_function
12
+
13
+ def add_error(instance, message, attribute: :base)
14
+ if Estate::Configuration.raise_on_error
15
+ exception_message = attribute == :base ? message : "#{attribute}: #{message}"
16
+ raise(StandardError, exception_message)
17
+ else
18
+ instance.errors.add(attribute, message) unless instance.errors[attribute].include?(message)
19
+ end
20
+ end
21
+
22
+ def get_states(instance)
23
+ from_state = instance.public_send("#{Estate::Configuration.column_name}_was")
24
+ to_state = instance.public_send(Estate::Configuration.column_name)
25
+ [from_state, to_state]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Logic
5
+ module CommonLogic
6
+ def validate_state_changes(instance, from_state, to_state)
7
+ if from_state == to_state
8
+ if from_state.nil? && !Estate::Configuration.allow_empty_initial_state
9
+ add_error(instance, "empty `#{Estate::Configuration.column_name}` is not allowed")
10
+ end
11
+ elsif to_state.nil?
12
+ add_error(instance, 'transition to empty state is not allowed')
13
+ elsif !Estate::StateMachine.state_exists?(to_state)
14
+ add_error(instance, "state `#{to_state}` is not defined")
15
+ elsif !transition_allowed?(from_state, to_state)
16
+ add_error(instance, "transition from `#{from_state}` to `#{to_state}` is not allowed",
17
+ attribute: Estate::Configuration.column_name)
18
+ end
19
+ end
20
+
21
+ def transition_allowed?(from_state, to_state)
22
+ from_state.nil? || Estate::StateMachine.transition_exists?(from_state, to_state)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Logic
5
+ module Core
6
+ module_function
7
+
8
+ def call(orm, instance)
9
+ require 'estate/logic/common_logic'
10
+ require File.join(File.dirname(__FILE__), orm, 'specific_logic')
11
+
12
+ extend Estate::Logic::CommonLogic
13
+ extend "Estate::Logic::#{orm.classify}::SpecificLogic".safe_constantize
14
+
15
+ validate_state_changes(instance, *get_states(instance))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Logic
5
+ module Sequel
6
+ module Setup
7
+ module_function
8
+
9
+ def call(base)
10
+ base.class_eval do
11
+ def validate
12
+ super
13
+
14
+ Estate::Logic::Core.call(Estate::Constants::Orm::SEQUEL, self)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'estate/logic/common_logic'
4
+
5
+ module Estate
6
+ module Logic
7
+ module Sequel
8
+ module SpecificLogic
9
+ extend Estate::Logic::CommonLogic
10
+
11
+ module_function
12
+
13
+ # TODO: remove :base
14
+ def add_error(instance, message, attribute: :base)
15
+ instance.errors.add(attribute, message)
16
+ end
17
+
18
+ def get_states(instance)
19
+ from_state, = instance.column_change(Estate::Configuration.column_name)
20
+ to_state = instance.values[Estate::Configuration.column_name]
21
+ [from_state, to_state]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ 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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Estate
4
+ module Setup
5
+ module_function
6
+
7
+ def call(base)
8
+ if base.ancestors.map(&:to_s).include? 'ActiveRecord::Base'
9
+ require File.join(File.dirname(__FILE__), 'logic', 'active_record', 'setup')
10
+ Estate::Logic::ActiveRecord::Setup.call(base)
11
+ else
12
+ require File.join(File.dirname(__FILE__), 'logic', 'sequel', 'setup')
13
+ Estate::Logic::Sequel::Setup.call(base)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -9,32 +9,27 @@ module Estate
9
9
  end
10
10
 
11
11
  def state_exists?(state)
12
- !state.nil? && states.key?(state.to_sym)
12
+ states.key?(state.to_sym)
13
13
  end
14
14
 
15
15
  def register_state(state)
16
- case state
17
- when Symbol
18
- states[state] = nil
19
- when String
20
- states[state.to_sym] = nil
21
- else
22
- raise(ArgumentError, 'State must be a Symbol or a String')
23
- end
16
+ states[state.to_sym] = nil
24
17
  end
25
18
 
26
- def transition_exists?(from:, to:)
27
- # TODO: validate from and to
28
- transition_key = { from: from.to_sym, to: to.to_sym }
19
+ def transition_exists?(from_state, to_state)
20
+ transition_key = { from: from_state.to_sym, to: to_state.to_sym }
29
21
  transitions.key?(transition_key)
30
22
  end
31
23
 
32
- def register_transition(from:, to:)
33
- # TODO: validate from and to
34
- transition_key = { from: from.to_sym, to: to.to_sym }
24
+ def register_transition(from_state, to_state)
25
+ transition_key = { from: from_state.to_sym, to: to_state.to_sym }
35
26
  transitions[transition_key] = nil
36
27
  end
37
28
 
29
+ def argument_valid?(argument)
30
+ argument.is_a?(Symbol) || argument.is_a?(String)
31
+ end
32
+
38
33
  attr_reader :states, :transitions
39
34
  end
40
35
  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.1'
5
5
  end
data/lib/estate.rb CHANGED
@@ -1,8 +1,10 @@
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/constants/orm'
8
5
  require 'estate/estate'
6
+ require 'estate/logic/core'
7
+ require 'estate/requirements'
8
+ require 'estate/setup'
9
+ require 'estate/state_machine'
10
+ 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.1
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-29 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,10 +90,17 @@ 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/constants/orm.rb
81
95
  - lib/estate/estate.rb
96
+ - lib/estate/logic/active_record/setup.rb
97
+ - lib/estate/logic/active_record/specific_logic.rb
98
+ - lib/estate/logic/common_logic.rb
99
+ - lib/estate/logic/core.rb
100
+ - lib/estate/logic/sequel/setup.rb
101
+ - lib/estate/logic/sequel/specific_logic.rb
82
102
  - lib/estate/requirements.rb
103
+ - lib/estate/setup.rb
83
104
  - lib/estate/state_machine.rb
84
105
  - lib/estate/version.rb
85
106
  homepage: https://github.com/igorkorepanov/estate
@@ -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