estate 0.1.0 → 0.2.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: a5e27471f690e289d113e8e69a6c72ad1c063a20b57bdead1f97040b71cb101f
4
- data.tar.gz: 233b56c93ade0cc71d91d721bd58f207bc17bc320b9e2165d31ad2a4700786d7
3
+ metadata.gz: 4b3da0543146b0a82972f87969bc2278b1deb7ab600965370caf143bd8b342d7
4
+ data.tar.gz: e87a26965c47b976746059431196048fb2b03cb829475e5e1eabf48b80054694
5
5
  SHA512:
6
- metadata.gz: c1330c6a3512171bdb6deaa89612fa4adf4d6aad6ef621e97aa456c0e792e48f21b1d7cac9ef47fe36a7a8b1806c6148da9b973200e1ff5f27e5434deac43c11
7
- data.tar.gz: 1452e5b9408d8cfd658ed983d11f1f8a0300e533400875285af65c84ee7a451edf6f29be6df4f779e991ce47a25fd6e230404b8949d37a15a1353414ef1be521
6
+ metadata.gz: bd904b3f3dbcf36895fb76c3c76ce05c7cb4af45ab30150fb8d9becd2120c9a7add11ada7051396e3f5cfc7093702221b7f6e77de994e04e347f1018802ce0f3
7
+ data.tar.gz: c69b6df5691fcc4104c28e9ce5eb23ad294c54002a8ff32c498750074c7c6bded4bbe7cb0707d69f3abd23ab4f47e67b59ca25571784c008e63c41135620e76e
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![CI](https://github.com/igorkorepanov/estate/actions/workflows/main.yml/badge.svg)
2
+
1
3
  # Estate Gem
2
4
 
3
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.
@@ -2,16 +2,6 @@
2
2
 
3
3
  module Estate
4
4
  module Configuration
5
- class << self
6
- def init_config(column_name:, allow_empty_initial_state:, raise_on_error:)
7
- @column_name = column_name
8
- @allow_empty_initial_state = allow_empty_initial_state
9
- @raise_on_error = raise_on_error
10
- end
11
-
12
- attr_reader :column_name, :allow_empty_initial_state, :raise_on_error
13
- end
14
-
15
5
  module Defaults
16
6
  COLUMN_NAME = :state
17
7
  ALLOW_EMPTY_INITIAL_STATE = false
@@ -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,38 +5,44 @@ module Estate
5
5
  base.extend Estate::ClassMethods
6
6
 
7
7
  Estate::Requirements.check_requirements(base)
8
- Estate::StateMachine.create_store
9
- Estate::Core.setup(base)
8
+ Estate::Setup.call(base)
10
9
  end
11
10
 
12
11
  module ClassMethods
13
12
  def estate(column: Estate::Configuration::Defaults::COLUMN_NAME,
14
13
  empty_initial_state: Estate::Configuration::Defaults::ALLOW_EMPTY_INITIAL_STATE,
15
14
  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)
15
+ Estate::StateMachine.init(name, column, empty_initial_state, raise_on_error)
18
16
 
19
17
  yield if block_given?
20
18
  end
21
19
 
22
- def state(name)
23
- if Estate::StateMachine.state_exists?(name)
24
- raise(StandardError, "state `:#{name}` is already defined")
20
+ def state(state_name = nil)
21
+ raise(StandardError, 'state must be a Symbol or a String') unless Estate::StateMachine.argument_valid?(state_name)
22
+
23
+ if Estate::StateMachine.state_exists?(name, state_name)
24
+ raise(StandardError, "state `:#{state_name}` is already defined")
25
25
  else
26
- Estate::StateMachine.register_state(name)
26
+ Estate::StateMachine.register_state(name, state_name)
27
27
  end
28
28
  end
29
29
 
30
- def transition(from:, to:)
31
- raise(StandardError, "state `#{from}` is not defined") unless Estate::StateMachine.state_exists?(from)
30
+ def transition(from: nil, to: nil)
31
+ unless Estate::StateMachine.argument_valid?(from)
32
+ raise(StandardError, 'argument `from` must be a Symbol or a String')
33
+ end
34
+
35
+ raise(StandardError, 'argument `to` must be a Symbol or a String') unless Estate::StateMachine.argument_valid?(to)
36
+
37
+ raise(StandardError, "state `#{from}` is not defined") unless Estate::StateMachine.state_exists?(name, from)
32
38
 
33
- raise(StandardError, "state `#{to}` is not defined") unless Estate::StateMachine.state_exists?(to)
39
+ raise(StandardError, "state `#{to}` is not defined") unless Estate::StateMachine.state_exists?(name, to)
34
40
 
35
- if Estate::StateMachine.transition_exists?(from: from, to: to)
41
+ if Estate::StateMachine.transition_exists?(name, from, to)
36
42
  raise(StandardError, "`transition from: :#{from}, to: :#{to}` already defined")
37
43
  end
38
44
 
39
- Estate::StateMachine.register_transition(from: from, to: to)
45
+ Estate::StateMachine.register_transition(name, from, to)
40
46
  end
41
47
  end
42
48
  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 config_for(instance)[: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("#{config_for(instance)[:column_name]}_was")
24
+ to_state = instance.public_send(config_for(instance)[:column_name])
25
+ [from_state, to_state]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
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
+ state_machine_name = instance.class.name
8
+
9
+ if from_state == to_state
10
+ if from_state.nil? && !config_for(instance)[:empty_initial_state]
11
+ add_error(instance, "empty `#{config_for(instance)[:column_name]}` is not allowed")
12
+ end
13
+ elsif to_state.nil?
14
+ add_error(instance, 'transition to empty state is not allowed')
15
+ elsif !Estate::StateMachine.state_exists?(state_machine_name, to_state)
16
+ add_error(instance, "state `#{to_state}` is not defined")
17
+ elsif !transition_allowed?(state_machine_name, from_state, to_state)
18
+ add_error(instance, "transition from `#{from_state}` to `#{to_state}` is not allowed",
19
+ attribute: config_for(instance)[:column_name])
20
+ end
21
+ end
22
+
23
+ def transition_allowed?(state_machine_name, from_state, to_state)
24
+ from_state.nil? || Estate::StateMachine.transition_exists?(state_machine_name, from_state, to_state)
25
+ end
26
+
27
+ def config_for(instance)
28
+ state_machine_name = instance.class.name
29
+ Estate::StateMachine.state_machines[state_machine_name][:config]
30
+ end
31
+ end
32
+ end
33
+ 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(config_for(instance)[:column_name])
20
+ to_state = instance.values[config_for(instance)[:column_name]]
21
+ [from_state, to_state]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -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
@@ -3,38 +3,42 @@
3
3
  module Estate
4
4
  class StateMachine
5
5
  class << self
6
- def create_store
7
- @states = {}
8
- @transitions = {}
6
+ def init(state_machine_name, column_name, empty_initial_state, raise_on_error)
7
+ @state_machines ||= {}
8
+ @state_machines[state_machine_name] = {
9
+ config: {
10
+ column_name: column_name,
11
+ empty_initial_state: empty_initial_state, # TODO: allow_empty_initial_state ?
12
+ raise_on_error: raise_on_error
13
+ },
14
+ states: {},
15
+ transitions: {}
16
+ }
9
17
  end
10
18
 
11
- def state_exists?(state)
12
- !state.nil? && states.key?(state.to_sym)
19
+ def state_exists?(state_machine_name, state_name)
20
+ state_machines[state_machine_name][:states].key?(state_name.to_sym)
13
21
  end
14
22
 
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
23
+ def register_state(state_machine_name, state_name)
24
+ state_machines[state_machine_name][:states][state_name.to_sym] = nil
24
25
  end
25
26
 
26
- def transition_exists?(from:, to:)
27
- transition_key = { from: from.to_sym, to: to.to_sym }
28
- transitions.key?(transition_key)
27
+ def transition_exists?(state_machine_name, from_state, to_state)
28
+ transition_key = { from: from_state.to_sym, to: to_state.to_sym }
29
+ state_machines[state_machine_name][:transitions].key?(transition_key)
29
30
  end
30
31
 
31
- def register_transition(from:, to:)
32
- # TODO: validate from and to
33
- transition_key = { from: from.to_sym, to: to.to_sym }
34
- transitions[transition_key] = nil
32
+ def register_transition(state_machine_name, from_state, to_state)
33
+ transition_key = { from: from_state.to_sym, to: to_state.to_sym }
34
+ state_machines[state_machine_name][:transitions][transition_key] = nil
35
35
  end
36
36
 
37
- attr_reader :states, :transitions
37
+ def argument_valid?(argument)
38
+ argument.is_a?(Symbol) || argument.is_a?(String)
39
+ end
40
+
41
+ attr_reader :state_machines
38
42
  end
39
43
  end
40
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Estate
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/estate.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'estate/configuration'
4
- require 'estate/core'
4
+ require 'estate/constants/orm'
5
5
  require 'estate/estate'
6
+ require 'estate/logic/core'
6
7
  require 'estate/requirements'
8
+ require 'estate/setup'
7
9
  require 'estate/state_machine'
8
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.1.0
4
+ version: 0.2.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-27 00:00:00.000000000 Z
11
+ date: 2024-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -91,11 +91,16 @@ files:
91
91
  - README.md
92
92
  - lib/estate.rb
93
93
  - lib/estate/configuration.rb
94
- - lib/estate/core.rb
95
- - lib/estate/db/active_record.rb
96
- - lib/estate/db/sequel.rb
94
+ - lib/estate/constants/orm.rb
97
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
98
102
  - lib/estate/requirements.rb
103
+ - lib/estate/setup.rb
99
104
  - lib/estate/state_machine.rb
100
105
  - lib/estate/version.rb
101
106
  homepage: https://github.com/igorkorepanov/estate
data/lib/estate/core.rb DELETED
@@ -1,17 +0,0 @@
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
@@ -1,47 +0,0 @@
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
@@ -1,45 +0,0 @@
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