rdux 0.2.1 → 0.3.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: f80889d90e6024e5de59e51ee24c2e91dc87499b0dedd84ad0ac3d15a1358226
4
- data.tar.gz: b4063bb3cf881541261be635b53820a93b59e6844f8ded7d8424e1dcd9951176
3
+ metadata.gz: 56611332f7071f2e1b9975e05c8886ff05f6b7107817ee5b71bb36d97e6b720d
4
+ data.tar.gz: 210f46fd573288d20dbdc38f4fa62c3ecef064ff7cafa48ee0f8549ab631ebcc
5
5
  SHA512:
6
- metadata.gz: 2f187a3b1131c8db35fe7f09fc7bb284dba17f1c32b78874ccd567592f0bbaba33a1ab7b34b6b7a94874f0153f76a84215afc2e7ac3e34ed4d1a360d424a300e
7
- data.tar.gz: b22cc76bbcbbe0dcc55806153ee92705c7054ce5a45486c3241cc1f3f62cf719b5da16fe579481c40107756ae1fc2f2177ce9394b880563ffc46a8d81df3950f
6
+ metadata.gz: 140e8bb81c6ad284a931a3d5565777c76e57bbc20405e8220846e2d4691635939f4fa0069ce2a22756b5f79e4788a9a873895de36a7b5f7ad07cac3b23d28d35
7
+ data.tar.gz: e61e3931c6ddd9b206ba58d1eb95d2ca375f6343a61c293198bb24be9f119f2020aebee9a35be2aa664ea59b1a8b8f3372b394b2e2e0748fcde61c17025c3e47
data/README.md CHANGED
@@ -1,10 +1,26 @@
1
1
  # Rdux
2
- Short description and motivation.
2
+ Minimal take on event sourcing.
3
3
 
4
4
  ## Usage
5
5
 
6
6
  ```bash
7
7
  $ bin/rails rdux:install:migrations
8
+ $ bin/rails db:migrate
9
+ ```
10
+
11
+ ### Code structure
12
+
13
+ ### Dispatch action
14
+
15
+ ```ruby
16
+ Rdux.perform(
17
+ Activity::Stop,
18
+ { activity_id: current_activity.id },
19
+ { activity: current_activity },
20
+ meta: {
21
+ stream: { user_id: 123, context: 'foo' }, bar: 'baz'
22
+ }
23
+ )
8
24
  ```
9
25
 
10
26
  ## Installation
@@ -27,11 +43,9 @@ $ gem install rdux
27
43
  ## Test
28
44
 
29
45
  ```bash
30
- $ bin/test
46
+ $ DB=postgres bin/test
47
+ $ DB=sqlite bin/test
31
48
  ```
32
49
 
33
- ## Contributing
34
- Contribution directions go here.
35
-
36
50
  ## License
37
51
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -26,4 +26,6 @@ Rake::TestTask.new(:test) do |t|
26
26
  t.verbose = false
27
27
  end
28
28
 
29
+ Dir.glob('lib/tasks/**/*.rake').each { |r| import r }
30
+
29
31
  task default: :test
@@ -2,32 +2,75 @@
2
2
 
3
3
  module Rdux
4
4
  class Action < ApplicationRecord
5
- def self.table_name_prefix
6
- 'rdux_'
7
- end
5
+ include Actionable
6
+
7
+ belongs_to :rdux_failed_action, optional: true, class_name: 'Rdux::FailedAction'
8
+ belongs_to :rdux_action, optional: true, class_name: 'Rdux::Action'
9
+ has_many :rdux_actions, class_name: 'Rdux::Action', foreign_key: 'rdux_action_id'
8
10
 
9
- serialize :up_payload, JSON
10
11
  serialize :down_payload, JSON
11
12
 
12
- validates :name, presence: true
13
- validates :up_payload, presence: true
13
+ scope :up, -> { where(down_at: nil) }
14
+ scope :down, -> { where.not(down_at: nil) }
14
15
 
15
- def up(opts)
16
- if opts.any?
17
- action_creator.up(up_payload, opts)
18
- else
19
- action_creator.up(up_payload)
20
- end
16
+ def call(opts = {})
17
+ perform_action(:call, up_payload, opts)
18
+ end
19
+
20
+ def up(opts = {})
21
+ return false if up_payload_sanitized
22
+ return false unless down_at.nil?
23
+
24
+ perform_action(:up, up_payload, opts)
21
25
  end
22
26
 
23
27
  def down
24
- action_creator.down(down_payload)
28
+ return false unless down_at.nil?
29
+ return false unless can_down?
30
+
31
+ perform_action(:down, down_payload, build_opts)
32
+ update(down_at: Time.current)
33
+ end
34
+
35
+ def to_failed_action
36
+ FailedAction.new(attributes.except('down_payload', 'down_at', 'rdux_action_id'))
25
37
  end
26
38
 
27
39
  private
28
40
 
29
- def action_creator
30
- name.to_s.classify.constantize.new
41
+ def can_down?
42
+ q = self.class.where('created_at > ?', created_at)
43
+ .where(down_at: nil)
44
+ .where('id != ?', rdux_action_id.to_i)
45
+ q = q.where(stream_hash: stream_hash) unless stream_hash.nil?
46
+ !q.count.positive?
47
+ end
48
+
49
+ def action_creator(meth)
50
+ name_const = name.to_s.classify.constantize
51
+ return name_const if name_const.respond_to?(meth)
52
+ return unless name_const.is_a?(Class)
53
+
54
+ obj = name_const.new
55
+ obj.respond_to?(meth) ? obj : nil
56
+ end
57
+
58
+ def perform_action(meth, payload, opts)
59
+ responder = action_creator(meth)
60
+ return if responder.nil?
61
+
62
+ if opts.any?
63
+ responder.public_send(meth, payload, opts)
64
+ else
65
+ responder.public_send(meth, payload)
66
+ end
67
+ end
68
+
69
+ def build_opts
70
+ {}.tap do |h|
71
+ nested = rdux_actions.order(:created_at)
72
+ h[:nested] = nested if nested.any?
73
+ end
31
74
  end
32
75
  end
33
76
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdux
4
+ module Actionable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ serialize :up_payload, JSON
9
+ serialize :up_result, JSON
10
+ serialize :meta, JSON
11
+
12
+ validates :name, presence: true
13
+ validates :up_payload, presence: true
14
+
15
+ before_save do
16
+ if meta_changed? && meta['stream'] && (meta_was || {})['stream'] != meta['stream']
17
+ self.stream_hash = Digest::SHA256.hexdigest(meta['stream'].to_json)
18
+ end
19
+ end
20
+ end
21
+
22
+ class_methods do
23
+ def table_name_prefix
24
+ 'rdux_'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdux
4
+ class FailedAction < ApplicationRecord
5
+ include Actionable
6
+
7
+ belongs_to :rdux_failed_action, optional: true, class_name: 'Rdux::FailedAction'
8
+ has_many :rdux_failed_actions, class_name: 'Rdux::FailedAction', foreign_key: 'rdux_failed_action_id'
9
+ has_many :rdux_actions, class_name: 'Rdux::Action', foreign_key: 'rdux_failed_action_id'
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRduxFailedActions < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :rdux_failed_actions do |t|
6
+ t.string :name, null: false
7
+ t.column :up_payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text), null: false
8
+ t.boolean :up_payload_sanitized, default: false
9
+ t.column :up_result, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
10
+ t.column :meta, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
11
+ t.string :stream_hash
12
+
13
+ t.belongs_to :rdux_failed_action, index: true, foreign_key: true
14
+
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRduxActions < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :rdux_actions do |t|
6
+ t.string :name, null: false
7
+ t.column :up_payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text), null: false
8
+ t.column :down_payload, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
9
+ t.datetime :down_at
10
+ t.boolean :up_payload_sanitized, default: false
11
+ t.column :up_result, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
12
+ t.column :meta, (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' ? :jsonb : :text)
13
+ t.string :stream_hash
14
+
15
+ t.belongs_to :rdux_action, index: true, foreign_key: true
16
+ t.belongs_to :rdux_failed_action, index: true, foreign_key: true
17
+
18
+ t.timestamps
19
+ end
20
+ end
21
+ end
data/lib/rdux/result.rb CHANGED
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdux
4
- Result = Struct.new(:ok, :down_payload, :resp, :action) do
4
+ Result = Struct.new(:ok, :down_payload, :resp, :action, :up_result, :nested, :save) do
5
5
  def payload
6
6
  resp || down_payload
7
7
  end
8
+
9
+ def save_failed?
10
+ ok == false && save
11
+ end
8
12
  end
9
13
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdux
4
+ module Sanitize
5
+ class << self
6
+ def call(payload)
7
+ filtered_payload = payload.deep_dup # Create a duplicate to avoid modifying the original params
8
+ Rails.application.config.filter_parameters.each do |filter_param|
9
+ filter_recursive(filtered_payload, filter_param)
10
+ end
11
+ filtered_payload
12
+ end
13
+
14
+ private
15
+
16
+ def filter_recursive(payload, filter_param)
17
+ payload.each do |key, value|
18
+ if value.is_a? Hash # If the value is a nested parameter
19
+ filter_recursive(value, filter_param)
20
+ elsif key == filter_param.to_s # to_s is used to ensure that symbol/string difference does not matter
21
+ payload[key] = '[FILTERED]'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/rdux/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdux
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/rdux.rb CHANGED
@@ -1,19 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rdux/engine'
4
-
5
4
  require 'rdux/result'
5
+ require 'rdux/sanitize'
6
+ require 'active_support/concern'
6
7
 
7
8
  module Rdux
8
- module_function
9
-
10
- def dispatch(action_name, payload, opts = {})
11
- action = Action.new(name: action_name, up_payload: payload)
12
- res = action.up(opts)
13
- action.down_payload = res.down_payload
14
- res.down_payload = action.down_payload
15
- action.save! if res.ok
16
- res.action = action
17
- res
9
+ class << self
10
+ def dispatch(action_name, payload, opts = {}, meta: nil)
11
+ (opts[:ars] || {}).each { |k, v| payload["#{k}_id"] = v.id }
12
+ action = Action.new(name: action_name, up_payload: payload, meta: meta)
13
+ call_call_meth_on_action(action, opts)
14
+ end
15
+
16
+ alias perform dispatch
17
+
18
+ private
19
+
20
+ def call_call_meth_on_action(action, opts)
21
+ res = action.call(opts)
22
+ return call_up_meth_on_action(action, opts) if res.nil?
23
+
24
+ unless res.down_payload.nil?
25
+ res.resp = res.down_payload.deep_stringify_keys
26
+ res.down_payload = nil
27
+ end
28
+ assign_and_persist(res, action)
29
+ end
30
+
31
+ def call_up_meth_on_action(action, opts)
32
+ res = action.up(opts)
33
+ res.down_payload&.deep_stringify_keys!
34
+ action.down_payload = res.down_payload
35
+ assign_and_persist(res, action)
36
+ end
37
+
38
+ def assign_and_persist(res, action)
39
+ if res.ok
40
+ assign_and_persist_for_ok(res, action)
41
+ elsif res.save_failed?
42
+ assign_and_persist_for_failed(res, action)
43
+ end
44
+ res.action ||= action
45
+ res
46
+ end
47
+
48
+ def assign_and_persist_common(res, action)
49
+ sanitize(action)
50
+ action.up_result = res.up_result
51
+ end
52
+
53
+ def assign_and_persist_for_ok(res, action)
54
+ assign_and_persist_common(res, action)
55
+ res.nested&.each { |nested_res| action.rdux_actions << nested_res.action }
56
+ action.save!
57
+ end
58
+
59
+ def assign_and_persist_for_failed(res, action)
60
+ assign_and_persist_common(res, action)
61
+ action.up_result ||= res.resp
62
+ res.action = action.to_failed_action.tap(&:save!)
63
+ assign_nested_responses_to_failed_action(res.action, res.nested) unless res.nested.nil?
64
+ end
65
+
66
+ def assign_nested_responses_to_failed_action(failed_action, nested)
67
+ nested.each do |nested_res|
68
+ if nested_res.action.is_a?(Rdux::Action)
69
+ failed_action.rdux_actions << nested_res.action
70
+ else
71
+ failed_action.rdux_failed_actions << nested_res.action
72
+ end
73
+ end
74
+ end
75
+
76
+ def sanitize(action)
77
+ up_payload_sanitized = Sanitize.call(action.up_payload)
78
+ action.up_payload_sanitized = action.up_payload != up_payload_sanitized
79
+ action.up_payload = up_payload_sanitized
80
+ end
18
81
  end
19
82
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :test do
4
+ desc 'Run tests with the SQLite database'
5
+ task :sqlite do
6
+ # Set an environment variable to change the database configuration
7
+ ENV['DB'] = 'sqlite'
8
+ Rake::Task['test'].invoke
9
+ end
10
+
11
+ desc 'Run tests with the Postgres database'
12
+ task :postgres do
13
+ ENV['DB'] = 'postgres'
14
+ Rake::Task['test'].invoke
15
+ end
16
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rdux
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zbigniew Humeniuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-28 00:00:00.000000000 Z
11
+ date: 2023-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -30,48 +30,62 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '8.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: pg
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.5.2
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.5.2
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: rubocop
35
49
  requirement: !ruby/object:Gem::Requirement
36
50
  requirements:
37
- - - "~>"
51
+ - - ">="
38
52
  - !ruby/object:Gem::Version
39
- version: '1.48'
53
+ version: 1.54.1
40
54
  type: :development
41
55
  prerelease: false
42
56
  version_requirements: !ruby/object:Gem::Requirement
43
57
  requirements:
44
- - - "~>"
58
+ - - ">="
45
59
  - !ruby/object:Gem::Version
46
- version: '1.48'
60
+ version: 1.54.1
47
61
  - !ruby/object:Gem::Dependency
48
62
  name: rubocop-rails
49
63
  requirement: !ruby/object:Gem::Requirement
50
64
  requirements:
51
- - - "~>"
65
+ - - ">="
52
66
  - !ruby/object:Gem::Version
53
- version: '2.18'
67
+ version: 2.20.2
54
68
  type: :development
55
69
  prerelease: false
56
70
  version_requirements: !ruby/object:Gem::Requirement
57
71
  requirements:
58
- - - "~>"
72
+ - - ">="
59
73
  - !ruby/object:Gem::Version
60
- version: '2.18'
74
+ version: 2.20.2
61
75
  - !ruby/object:Gem::Dependency
62
76
  name: sqlite3
63
77
  requirement: !ruby/object:Gem::Requirement
64
78
  requirements:
65
- - - "~>"
79
+ - - ">="
66
80
  - !ruby/object:Gem::Version
67
- version: 1.6.2
81
+ version: 1.6.3
68
82
  type: :development
69
83
  prerelease: false
70
84
  version_requirements: !ruby/object:Gem::Requirement
71
85
  requirements:
72
- - - "~>"
86
+ - - ">="
73
87
  - !ruby/object:Gem::Version
74
- version: 1.6.2
88
+ version: 1.6.3
75
89
  description: |
76
90
  Write apps that are easy to test.
77
91
  Rdux gives you a possibility to centralize your app's state modification logic (DB changes).
@@ -88,11 +102,16 @@ files:
88
102
  - README.md
89
103
  - Rakefile
90
104
  - app/models/rdux/action.rb
91
- - db/migrate/20200823045609_create_rdux_actions.rb
105
+ - app/models/rdux/actionable.rb
106
+ - app/models/rdux/failed_action.rb
107
+ - db/migrate/20230621215717_create_rdux_failed_actions.rb
108
+ - db/migrate/20230621215718_create_rdux_actions.rb
92
109
  - lib/rdux.rb
93
110
  - lib/rdux/engine.rb
94
111
  - lib/rdux/result.rb
112
+ - lib/rdux/sanitize.rb
95
113
  - lib/rdux/version.rb
114
+ - lib/tasks/test.rake
96
115
  homepage: https://artofcode.co
97
116
  licenses:
98
117
  - MIT
@@ -113,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
132
  - !ruby/object:Gem::Version
114
133
  version: '0'
115
134
  requirements: []
116
- rubygems_version: 3.4.6
135
+ rubygems_version: 3.4.10
117
136
  signing_key:
118
137
  specification_version: 4
119
138
  summary: Rdux adds a new layer to Rails apps - actions.
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRduxActions < ActiveRecord::Migration[6.0]
4
- def change
5
- create_table :rdux_actions do |t|
6
- t.string :name
7
- t.text :up_payload
8
- t.text :down_payload
9
-
10
- t.timestamps
11
- end
12
- end
13
- end