active_record-associated_object 0.2.0 → 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: d7a508d24fc9467600956a207ee83775b3618ccfc8be21a9c11e1caa72a11172
4
- data.tar.gz: '0360292507cf0e901140b9fe4003bd33aca10aff9d91b01b759e4ce75497a61f'
3
+ metadata.gz: 9b5e7079fe6ffefe707c65a2a88b92c20ff3c8b91601cdef75bd638adbb73166
4
+ data.tar.gz: 1e59dbc65c54baa19c8bac770ebd52a469aa95855c7758c020bfd8b72a51b3f0
5
5
  SHA512:
6
- metadata.gz: '04940ddec3fc5446ba945df9f3daeff22550e81cf55f1e61e02b79af687adfc376f05e1148a6fb0e7ff17ab5df1a4539fe1d1c8bde456b986561df8ef4714e93'
7
- data.tar.gz: 49ff34bb1945649479481ef7502585a124ee1464c67bc29f8ccb3335a53cfd0858790fbeebb976e3dfa1f055d12c085bb26ab2a7d054bed2ef51420f40cf83d5
6
+ metadata.gz: 7f2c7de0292ac2ce3ed011be69f6c887cf93873ab4b80c2288b4cd66ff22ec0780cb9872476656227eec164cf73ed9fc2e0ced1dcde154db6377343c408fbef5
7
+ data.tar.gz: 783ca5c727bb5718aa7436b0252b83b9f4c904f28829635cc117f83920a740d2361cce9708707b8ba2f084148b6a5968e6e6a6f8cca56d9b9526d4b30fa59a76
data/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## [0.3.0] - 2022-09-25
2
+
3
+ - Add `performs` to help cut down Active Job boilerplate.
4
+
5
+ ```ruby
6
+ class Post::Publisher < ActiveRecord::AssociatedObject
7
+ performs :publish, queue_as: :important
8
+
9
+ def publish
10
+
11
+ end
12
+ end
13
+ ```
14
+
15
+ The above is the same as writing:
16
+
17
+ ```ruby
18
+ class Post::Publisher < ActiveRecord::AssociatedObject
19
+ class Job < ApplicationJob; end
20
+ class PublishJob < Job
21
+ queue_as :important
22
+
23
+ def perform(publisher, *arguments, **options)
24
+ publisher.publish(*arguments, **options)
25
+ end
26
+ end
27
+
28
+ def publish_later(*arguments, **options)
29
+ PublishJob.perform_later(self, *arguments, **options)
30
+ end
31
+
32
+ def publish
33
+
34
+ end
35
+ end
36
+ ```
37
+
38
+ See the README for more details.
39
+
1
40
  ## [0.2.0] - 2022-04-21
2
41
 
3
42
  - Require a `has_object` call on the record side to associate an object.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_record-associated_object (0.2.0)
4
+ active_record-associated_object (0.3.0)
5
5
  activerecord (>= 6.1)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -8,31 +8,89 @@ Associate a Ruby PORO with an Active Record class and have it quack like one. Bu
8
8
  # app/models/post.rb
9
9
  class Post < ActiveRecord::Base
10
10
  # `has_object` defines a `publisher` method that calls Post::Publisher.new(post).
11
- has_object :publisher, after_touch: true, before_destroy: :prevent_post_destroy
11
+ has_object :publisher
12
12
  end
13
13
 
14
- # Create a standard PORO, but derive attributes from the Post:: namespace and its primary key.
14
+ # app/models/post/publisher.rb
15
+ class Post::Publisher
16
+ def initialize(post)
17
+ @post = post
18
+ end
19
+ end
20
+ ```
21
+
22
+ If you want Active Job, GlobalID and Kredis integration you can also have `Post::Publisher` inherit from `ActiveRecord::AssociatedObject`. This extends the standard PORO with details from the `Post::` namespace and the post primary key.
23
+
24
+ ```ruby
15
25
  # app/models/post/publisher.rb
16
26
  class Post::Publisher < ActiveRecord::AssociatedObject
27
+ # ActiveRecord::AssociatedObject defines initialize(post) automatically. It's derived from the `Post::` namespace.
28
+
17
29
  kredis_datetime :publish_at # Kredis integration generates a "post:publishers:<post_id>:publish_at" key.
18
30
 
19
- # Both a general `record` method and a `post` method is available to access the associated post.
20
- def publish_now
31
+ # `performs` builds a `Post::Publisher::PublishJob` and routes configs over to it.
32
+ performs :publish, queue_as: :important, discard_on: SomeError do
33
+ retry_on TimeoutError, wait: :exponentially_longer
34
+ end
35
+
36
+ def publish
37
+ # `transaction` is syntactic sugar for `post.transaction` here.
21
38
  transaction do
39
+ # A `post` method is generated to access the associated post. There's also a `record` alias available.
22
40
  post.update! published: true
23
41
  post.subscribers.post_published post
24
42
  end
25
43
  end
44
+ end
45
+ ```
46
+
47
+ ### How `performs` removes Active Job boilerplate
48
+
49
+ With an associated object like this:
50
+
51
+ ```ruby
52
+ class Post::Publisher < ActiveRecord::AssociatedObject
53
+ performs queue_as: :important
54
+ performs :publish
55
+ performs :retract
56
+
57
+ def publish
58
+ end
26
59
 
27
- def publish_later
28
- PublishJob.set(wait_until: publish_at).perform_later self
60
+ def retract(reason:)
29
61
  end
30
62
  end
63
+ ```
64
+
65
+ is equivalent to:
66
+
67
+ ```ruby
68
+ class Post::Publisher < ActiveRecord::AssociatedObject
69
+ # `performs` without a method defines a general job to share between method jobs.
70
+ class Job < ApplicationJob
71
+ queue_as :important
72
+ end
73
+
74
+ # Individual method jobs inherit from the `Post::Publisher::Job` defined above.
75
+ class PublishJob < Job
76
+ def perform(publisher, *arguments, **options)
77
+ # GlobalID integration means associated objects can be passed into jobs like Active Records, i.e. we don't have to do `post.publisher`.
78
+ publisher.publish(*arguments, **options)
79
+ end
80
+ end
31
81
 
32
- class Post::Publisher::PublishJob < ActiveJob::Base
33
- def perform(publisher)
34
- # Automatic integration via GlobalID means you don't have to do `post.publisher`.
35
- publisher.publish_now
82
+ class RetractJob < Job
83
+ def perform(publisher, *arguments, **options)
84
+ publisher.retract(*arguments, **options)
85
+ end
86
+ end
87
+
88
+ def publish_later(*arguments, **options)
89
+ PublishJob.perform_later(self, *arguments, **options)
90
+ end
91
+
92
+ def retract_later(*arguments, **options)
93
+ RetractJob.perform_later(self, *arguments, **options)
36
94
  end
37
95
  end
38
96
  ```
@@ -44,7 +102,7 @@ end
44
102
  ```ruby
45
103
  class Post < ActiveRecord::Base
46
104
  # Callbacks can be passed too to a specific method.
47
- has_object :publisher, after_touch: true, before_destroy: :prevent_post_destroy
105
+ has_object :publisher, after_touch: true, before_destroy: :prevent_errant_post_destroy
48
106
 
49
107
  # The above is the same as writing:
50
108
  after_touch { publisher.after_touch }
@@ -58,11 +116,17 @@ class Post::Publisher < ActiveRecord::AssociatedObject
58
116
 
59
117
  def prevent_errant_post_destroy
60
118
  # Passed callbacks can throw :abort too, and in this example prevent post.destroy.
61
- throw :abort unless haha_business?
119
+ throw :abort if haha_business?
62
120
  end
63
121
  end
64
122
  ```
65
123
 
124
+ ## Risks of depending on this gem
125
+
126
+ This gem is relatively tiny and I'm not expecting more significant changes on it, for right now. It's unofficial and not affiliated with Rails core.
127
+
128
+ Though it's written and maintained by an ex-Rails core person, so I know my way in and out of Rails and how to safely extend it.
129
+
66
130
  ## Installation
67
131
 
68
132
  Install the gem and add to the application's Gemfile by executing:
@@ -81,7 +145,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
81
145
 
82
146
  ## Contributing
83
147
 
84
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_record-associated_object.
148
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kaspth/active_record-associated_object.
85
149
 
86
150
  ## License
87
151
 
@@ -1,17 +1,23 @@
1
1
  module ActiveRecord::AssociatedObject::ObjectAssociation
2
+ using Module.new {
3
+ refine Module do
4
+ def extend_source_from(chunks, &block)
5
+ location = caller_locations(1, 1).first
6
+ source_chunks = Array(chunks).flat_map(&block)
7
+ class_eval source_chunks.join("\n\n"), location.path, location.lineno
8
+ end
9
+ end
10
+ }
11
+
2
12
  def has_object(*names, **callbacks)
3
- methods = names.map do |name|
13
+ extend_source_from(names) do |name|
4
14
  "def #{name}; @#{name} ||= #{self.name}::#{name.to_s.classify}.new(self); end"
5
15
  end
6
16
 
7
- class_eval methods.join("\n\n"), __FILE__, __LINE__ + 1
8
-
9
- passes = names.flat_map do |name|
17
+ extend_source_from(names) do |name|
10
18
  callbacks.map do |callback, method|
11
19
  "#{callback} { #{name}.#{method == true ? callback : method} }"
12
20
  end
13
21
  end
14
-
15
- class_eval passes.join("\n\n"), __FILE__, __LINE__ + 1
16
22
  end
17
23
  end
@@ -0,0 +1,36 @@
1
+ module ActiveRecord::AssociatedObject::Performs
2
+ def performs(method = nil, **configs, &block)
3
+ @job ||= safe_define("Job") { ApplicationJob }
4
+
5
+ if method.nil?
6
+ apply_performs_to(@job, **configs, &block)
7
+ else
8
+ job = safe_define("#{method}_job".classify) { @job }
9
+ apply_performs_to(job, **configs, &block)
10
+
11
+ job.class_eval <<~RUBY, __FILE__, __LINE__ + 1 unless job.instance_method(:perform).owner == job
12
+ def perform(object, *arguments, **options)
13
+ object.#{method}(*arguments, **options)
14
+ end
15
+ RUBY
16
+
17
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
18
+ def #{method}_later(*arguments, **options)
19
+ #{job}.perform_later(self, *arguments, **options)
20
+ end
21
+ RUBY
22
+ end
23
+ end
24
+
25
+ private
26
+ def safe_define(name)
27
+ name.safe_constantize || const_set(name, Class.new(yield))
28
+ end
29
+
30
+ def apply_performs_to(job_class, **configs, &block)
31
+ job_class.class_eval do
32
+ configs.each { public_send(_1, _2) }
33
+ yield if block_given?
34
+ end
35
+ end
36
+ end
@@ -2,12 +2,17 @@ class ActiveRecord::AssociatedObject::Railtie < Rails::Railtie
2
2
  initializer "integrations.include" do
3
3
  ActiveRecord::AssociatedObject.include Kredis::Attributes if defined?(Kredis)
4
4
  ActiveRecord::AssociatedObject.include GlobalID::Identification if defined?(GlobalID)
5
+
6
+ ActiveSupport.on_load :active_job do
7
+ require "active_record/associated_object/performs"
8
+ ActiveRecord::AssociatedObject.extend ActiveRecord::AssociatedObject::Performs
9
+ end
5
10
  end
6
11
 
7
12
  initializer "object_association.setup" do
8
13
  ActiveSupport.on_load :active_record do
9
14
  require "active_record/associated_object/object_association"
10
- ActiveRecord::Base.extend ActiveRecord::AssociatedObject::ObjectAssociation
15
+ extend ActiveRecord::AssociatedObject::ObjectAssociation
11
16
  end
12
17
  end
13
18
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  class AssociatedObject
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record-associated_object
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasper Timm Hansen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-21 00:00:00.000000000 Z
11
+ date: 2022-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -40,6 +40,7 @@ files:
40
40
  - active_record-associated_object.gemspec
41
41
  - lib/active_record/associated_object.rb
42
42
  - lib/active_record/associated_object/object_association.rb
43
+ - lib/active_record/associated_object/performs.rb
43
44
  - lib/active_record/associated_object/railtie.rb
44
45
  - lib/active_record/associated_object/version.rb
45
46
  homepage: https://github.com/kaspth/active_record-associated_object
@@ -64,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
65
  - !ruby/object:Gem::Version
65
66
  version: '0'
66
67
  requirements: []
67
- rubygems_version: 3.3.11
68
+ rubygems_version: 3.3.21
68
69
  signing_key:
69
70
  specification_version: 4
70
71
  summary: Associate a Ruby PORO with an Active Record class and have it quack like