active_record-associated_object 0.2.0 → 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: 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