custom_active_record_observer 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: de2f53459d0571d2542de638f2f10e80637d20ff
4
+ data.tar.gz: f1a58b6f3014fbbbfb96eee376fa1a8de82c7110
5
+ SHA512:
6
+ metadata.gz: 91aaa0ca81ac659ef16b3f58169e3069ecae1d549ffe5b959391b87c34fc5b17234aeaf6b29c308cf0d60e23a6ce3ad52acdefebd56b5643210d5313426d161c
7
+ data.tar.gz: 7deb22d765fffe58c49902a163604823e88e22172fd5b8e0bdd63dcce2fe7b53960dd7e3a622a24c0cca60449c03154b089938812562e7885788d57dd5866f96
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # CustomActiveRecordObserver
2
+
3
+ `custom_active_record_observer` is a DSL to track create/update/destroy operations based on AR callbacks.
4
+
5
+ Its a simple DSL similar to the code below:
6
+
7
+ ```
8
+ CustomActiveRecordObserver.observe :EngagedUser, :CompanyOwner, handler: CustomerEventPublisher.new do
9
+ on_create do |customer|
10
+ Events::CustomerSignedUp.new(data: {customer_id: customer.id}
11
+ end
12
+
13
+ on_destroy do |customer|
14
+ Events::CustomerSignedOut.new(data: {…})
15
+ end
16
+
17
+ on_change :group_id do |customer|
18
+ Events::CustomerGroupChanged.new(data: {…})
19
+ end
20
+
21
+ end
22
+ ```
23
+
24
+ which you can put into a single initializer file instead of scattering it around and having it in each AR model.
25
+ That file is easier to remove in future. It incapsulates all the “inaccurate" dependencies.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ begin
9
+ require 'rdoc/task'
10
+ rescue LoadError
11
+ require 'rdoc/rdoc'
12
+ require 'rake/rdoctask'
13
+ RDoc::Task = Rake::RDocTask
14
+ end
15
+
16
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
17
+ load 'rails/tasks/engine.rake'
18
+
19
+ Bundler::GemHelper.install_tasks
20
+
@@ -0,0 +1,16 @@
1
+ require 'active_support/dependencies'
2
+
3
+ module CustomActiveRecordObserver
4
+ require_relative 'custom_active_record_observer/all'
5
+
6
+ mattr_accessor :schema
7
+ @@schema = Schema.new
8
+
9
+ def self.observe(*class_names, handler:, &block)
10
+ class_names.each do |class_name|
11
+ DSL.new(block).actions_and_rules.each do |(action, rule)|
12
+ schema.add_rule(class_name, action, rule, handler)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'models_extender'
2
+ require_relative 'observable'
3
+ require_relative 'engine'
4
+ require_relative 'rules/all'
5
+ require_relative 'not_nil'
6
+ require_relative 'handler'
7
+ require_relative 'dsl'
8
+ require_relative 'schema'
9
+ require_relative 'changes_tracker'
@@ -0,0 +1,16 @@
1
+ module CustomActiveRecordObserver
2
+ module ChangesTracker
3
+ def self.[](*method_names)
4
+ Module.new do
5
+ method_names.each do |name|
6
+ define_method name do |*args|
7
+ @_active_record_observer_changes ||= {}
8
+ @_active_record_observer_changes.merge!(changes)
9
+
10
+ super(*args)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ module CustomActiveRecordObserver
2
+ class DSL
3
+ def initialize(block)
4
+ instance_exec(&block)
5
+ end
6
+
7
+ def actions_and_rules
8
+ @actions_and_rules ||= []
9
+ end
10
+
11
+ def on_create(&block)
12
+ store(:create, Rules::CreateRule.new(block))
13
+ end
14
+
15
+ def on_destroy(&block)
16
+ store(:destroy, Rules::DestroyRule.new(block))
17
+ end
18
+
19
+ def on_update(*attributes, &block)
20
+ pattern = attributes.pop if attributes.last.is_a?(Array)
21
+
22
+ attributes.each do |attribute|
23
+ store(:update, Rules::UpdateRule.new(block, attribute: attribute, pattern: pattern))
24
+ end
25
+ end
26
+
27
+ def on_add(*attributes, &block)
28
+ on_update(*attributes.push([nil, NotNil]), &block)
29
+ end
30
+
31
+ def on_remove(*attributes, &block)
32
+ on_update(*attributes.push([NotNil, nil]), &block)
33
+ end
34
+
35
+ def on_change(*attributes, &block)
36
+ on_update(*attributes.push([NotNil, NotNil]), &block)
37
+ end
38
+
39
+ private
40
+
41
+ def store(action, rule)
42
+ actions_and_rules << [action, rule]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ module CustomActiveRecordObserver
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace CustomActiveRecordObserver
4
+
5
+ config.to_prepare do
6
+ CustomActiveRecordObserver.schema.freeze
7
+
8
+ CustomActiveRecordObserver::ModelsExtender.(CustomActiveRecordObserver.schema)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module CustomActiveRecordObserver
2
+ module Handler
3
+ def self.call(target, action, _changes, schema: CustomActiveRecordObserver.schema)
4
+ # Some AR libraries reload AR object in the callbacks.
5
+ # Hence, ActiveModel::Dirty model changes hash becomes empty.
6
+ # @_active_record_observer_changes is needed to that changes not be missed
7
+ changes =
8
+ (target.instance_variable_get(:@_active_record_observer_changes) || {}).merge(_changes)
9
+
10
+ ActiveRecord::Base.transaction do
11
+ schema.
12
+ get_rules(target.class.name, action, changes).
13
+ each { |rule, handler| handler.(rule.(target)) }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module CustomActiveRecordObserver
2
+ module ModelsExtender
3
+ def self.call(schema)
4
+ schema.classes.map(&:base_class).uniq.each do |base_class|
5
+ base_class.include(CustomActiveRecordObserver::Observable)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module CustomActiveRecordObserver
2
+ module NotNil
3
+ def self.===(other)
4
+ super || !other.nil?
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module CustomActiveRecordObserver
2
+ module Observable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_commit -> { CustomActiveRecordObserver::Handler.(self, :create, previous_changes) }, on: :create
7
+ after_commit -> { CustomActiveRecordObserver::Handler.(self, :update, previous_changes) }, on: :update
8
+ after_commit -> { CustomActiveRecordObserver::Handler.(self, :destroy, previous_changes) }, on: :destroy
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'base_rule'
2
+
3
+ Dir[File.expand_path('*.rb', __dir__)].each { |f| require f }
@@ -0,0 +1,19 @@
1
+ module CustomActiveRecordObserver
2
+ module Rules
3
+ class BaseRule
4
+ attr_reader :block
5
+
6
+ def initialize(block)
7
+ @block = block
8
+ end
9
+
10
+ def call(target)
11
+ block.call(target)
12
+ end
13
+
14
+ def allowed?(changes)
15
+ raise NotImplementedError
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module CustomActiveRecordObserver
2
+ module Rules
3
+ class CreateRule < BaseRule
4
+ def allowed?(changes)
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,10 @@
1
+ module CustomActiveRecordObserver
2
+ module Rules
3
+ class DestroyRule < BaseRule
4
+ def allowed?(changes)
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,32 @@
1
+ module CustomActiveRecordObserver
2
+ module Rules
3
+ class UpdateRule < BaseRule
4
+ attr_reader :attribute, :pattern
5
+
6
+ def initialize(block, attribute: nil, pattern: nil)
7
+ super(block)
8
+
9
+ if pattern.is_a?(Array) && pattern.size != 2
10
+ raise ArgumentError, "#{ pattern } is expected to be an array of two elements"
11
+ end
12
+
13
+ @attribute = attribute
14
+ @pattern = pattern
15
+ end
16
+
17
+ def allowed?(changes)
18
+ attribute &&
19
+ changes.has_key?(attribute) &&
20
+ matches_pattern?(*changes.fetch(attribute))
21
+ end
22
+
23
+ private
24
+
25
+ def matches_pattern?(value_was, value)
26
+ return true unless pattern # no pattern => allowed for all
27
+
28
+ pattern.first === value_was && pattern.last === value
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ module CustomActiveRecordObserver
2
+ class Schema
3
+ attr_reader :schema
4
+
5
+ delegate :fetch, to: :schema
6
+
7
+ # {
8
+ # 'User' =>
9
+ # {
10
+ # update: [[rule_1, handler], [rule_2, handler]],
11
+ # create: [[rule_3, handler]]
12
+ # }
13
+ # 'Article' =>
14
+ # {
15
+ # destroy: [[rule5, handler]],
16
+ # }
17
+ # }
18
+ def initialize(schema = {})
19
+ @schema = schema
20
+ end
21
+
22
+ def add_rule(class_name, action, rules, handler)
23
+ schema[class_name] ||= {}
24
+ schema[class_name][action] ||= []
25
+
26
+ Array(rules).each do |rule|
27
+ schema[class_name][action] << [rule, handler]
28
+ end
29
+ end
30
+
31
+ def get_rules(class_name, action, raw_changes = {})
32
+ changes = raw_changes.symbolize_keys
33
+
34
+ schema.fetch(class_name.to_sym, {}).
35
+ fetch(action, []).select do |(rule, handler)|
36
+ rule.allowed?(changes)
37
+ end
38
+ end
39
+
40
+ def classes
41
+ schema.keys.map { |name| name.to_s.constantize }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module CustomActiveRecordObserver
2
+ VERSION = '0.1.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: custom_active_record_observer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - OnApp Ltd.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-11-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6'
33
+ - !ruby/object:Gem::Dependency
34
+ name: railties
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.2'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '6'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.2'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '6'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rspec
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: sqlite3
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: fuubar
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: pry
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ description:
110
+ email:
111
+ - support@onapp.com
112
+ executables: []
113
+ extensions: []
114
+ extra_rdoc_files: []
115
+ files:
116
+ - README.md
117
+ - Rakefile
118
+ - lib/custom_active_record_observer.rb
119
+ - lib/custom_active_record_observer/all.rb
120
+ - lib/custom_active_record_observer/changes_tracker.rb
121
+ - lib/custom_active_record_observer/dsl.rb
122
+ - lib/custom_active_record_observer/engine.rb
123
+ - lib/custom_active_record_observer/handler.rb
124
+ - lib/custom_active_record_observer/models_extender.rb
125
+ - lib/custom_active_record_observer/not_nil.rb
126
+ - lib/custom_active_record_observer/observable.rb
127
+ - lib/custom_active_record_observer/rules/all.rb
128
+ - lib/custom_active_record_observer/rules/base_rule.rb
129
+ - lib/custom_active_record_observer/rules/create_rule.rb
130
+ - lib/custom_active_record_observer/rules/destroy_rule.rb
131
+ - lib/custom_active_record_observer/rules/update_rule.rb
132
+ - lib/custom_active_record_observer/schema.rb
133
+ - lib/custom_active_record_observer/version.rb
134
+ homepage:
135
+ licenses:
136
+ - Apache 2.0
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.5.2
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: CustomActiveRecordObserver
158
+ test_files: []