rdux 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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