active_record-associated_object 0.1.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: '049c38f6aeb242c8ccfee9c15d3e29ace1c8f69eaf67619cdb38b09e5dd97514'
4
- data.tar.gz: 911ce88d3257b0db22169f325ca2059b12ecdd8bb7121d7a37368588dce37262
3
+ metadata.gz: 9b5e7079fe6ffefe707c65a2a88b92c20ff3c8b91601cdef75bd638adbb73166
4
+ data.tar.gz: 1e59dbc65c54baa19c8bac770ebd52a469aa95855c7758c020bfd8b72a51b3f0
5
5
  SHA512:
6
- metadata.gz: 0ee5964826318cb28a0a1d4c9e0a383b157e4ec0f2dd908b944c5eec68ddc7179d0d43be37bb65316c91e42318a6104b67eccd7d72194b99b4f3284312204692
7
- data.tar.gz: 61df4cf5bc61dd0109650ce1aa95bc1f9e79fd99c2da6e2757429b71cda10b1423d6dda7f29310390fa0c5d01b0a5b77563de4151c277316e7a3e57edf16c699
6
+ metadata.gz: 7f2c7de0292ac2ce3ed011be69f6c887cf93873ab4b80c2288b4cd66ff22ec0780cb9872476656227eec164cf73ed9fc2e0ced1dcde154db6377343c408fbef5
7
+ data.tar.gz: 783ca5c727bb5718aa7436b0252b83b9f4c904f28829635cc117f83920a740d2361cce9708707b8ba2f084148b6a5968e6e6a6f8cca56d9b9526d4b30fa59a76
data/CHANGELOG.md CHANGED
@@ -1,4 +1,59 @@
1
- ## [Unreleased]
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
+
40
+ ## [0.2.0] - 2022-04-21
41
+
42
+ - Require a `has_object` call on the record side to associate an object.
43
+
44
+ ```ruby
45
+ class Post < ActiveRecord::Base
46
+ has_object :publisher
47
+ end
48
+ ```
49
+
50
+ - Allow `has_object` to pass callbacks onto the associated object.
51
+
52
+ ```ruby
53
+ class Post < ActiveRecord::Base
54
+ has_object :publisher, after_touch: true, before_destroy: :prevent_errant_post_destroy
55
+ end
56
+ ```
2
57
 
3
58
  ## [0.1.0] - 2022-04-19
4
59
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_record-associated_object (0.1.0)
4
+ active_record-associated_object (0.3.0)
5
5
  activerecord (>= 6.1)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -7,37 +7,126 @@ Associate a Ruby PORO with an Active Record class and have it quack like one. Bu
7
7
  ```ruby
8
8
  # app/models/post.rb
9
9
  class Post < ActiveRecord::Base
10
+ # `has_object` defines a `publisher` method that calls Post::Publisher.new(post).
11
+ has_object :publisher
10
12
  end
11
13
 
12
- # 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
13
25
  # app/models/post/publisher.rb
14
26
  class Post::Publisher < ActiveRecord::AssociatedObject
27
+ # ActiveRecord::AssociatedObject defines initialize(post) automatically. It's derived from the `Post::` namespace.
28
+
15
29
  kredis_datetime :publish_at # Kredis integration generates a "post:publishers:<post_id>:publish_at" key.
16
30
 
17
- # Both a general `record` method and a `post` method is available to access the associated post.
18
- 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.
19
38
  transaction do
39
+ # A `post` method is generated to access the associated post. There's also a `record` alias available.
20
40
  post.update! published: true
21
41
  post.subscribers.post_published post
22
42
  end
23
43
  end
44
+ end
45
+ ```
46
+
47
+ ### How `performs` removes Active Job boilerplate
24
48
 
25
- def publish_later
26
- PublishJob.set(wait_until: publish_at).perform_later self
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
59
+
60
+ def retract(reason:)
27
61
  end
28
62
  end
63
+ ```
64
+
65
+ is equivalent to:
29
66
 
30
- # Post now has a publisher too.
31
- Post.first.publisher # => Post::Publisher.new(self) # Where self is Post.first.
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
32
73
 
33
- class Post::Publisher::PublishJob < ActiveJob::Base
34
- def perform(publisher)
35
- # Automatic integration via GlobalID means you don't have to do `post.publisher`.
36
- publisher.publish_now
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
81
+
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)
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### Passing callbacks onto the associated object
99
+
100
+ `has_object` accepts a hash of callbacks to pass.
101
+
102
+ ```ruby
103
+ class Post < ActiveRecord::Base
104
+ # Callbacks can be passed too to a specific method.
105
+ has_object :publisher, after_touch: true, before_destroy: :prevent_errant_post_destroy
106
+
107
+ # The above is the same as writing:
108
+ after_touch { publisher.after_touch }
109
+ before_destroy { publisher.prevent_errant_post_destroy }
110
+ end
111
+
112
+ class Post::Publisher < ActiveRecord::AssociatedObject
113
+ def after_touch
114
+ # Respond to the after_touch on the Post.
115
+ end
116
+
117
+ def prevent_errant_post_destroy
118
+ # Passed callbacks can throw :abort too, and in this example prevent post.destroy.
119
+ throw :abort if haha_business?
37
120
  end
38
121
  end
39
122
  ```
40
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
+
41
130
  ## Installation
42
131
 
43
132
  Install the gem and add to the application's Gemfile by executing:
@@ -56,7 +145,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
56
145
 
57
146
  ## Contributing
58
147
 
59
- 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.
60
149
 
61
150
  ## License
62
151
 
@@ -0,0 +1,23 @@
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
+
12
+ def has_object(*names, **callbacks)
13
+ extend_source_from(names) do |name|
14
+ "def #{name}; @#{name} ||= #{self.name}::#{name.to_s.classify}.new(self); end"
15
+ end
16
+
17
+ extend_source_from(names) do |name|
18
+ callbacks.map do |callback, method|
19
+ "#{callback} { #{name}.#{method == true ? callback : method} }"
20
+ end
21
+ end
22
+ end
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,5 +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
10
+ end
11
+
12
+ initializer "object_association.setup" do
13
+ ActiveSupport.on_load :active_record do
14
+ require "active_record/associated_object/object_association"
15
+ extend ActiveRecord::AssociatedObject::ObjectAssociation
16
+ end
5
17
  end
6
18
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  class AssociatedObject
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -13,12 +13,6 @@ class ActiveRecord::AssociatedObject
13
13
  define_singleton_method(:record_klass) { record_klass }
14
14
  define_singleton_method(:attribute_name) { attribute_name }
15
15
  delegate :record_klass, :attribute_name, to: :class
16
-
17
- record_klass.class_eval <<~RUBY, __FILE__, __LINE__ + 1
18
- def #{attribute_name}
19
- @#{attribute_name} ||= #{klass}.new(self)
20
- end
21
- RUBY
22
16
  end
23
17
 
24
18
  def respond_to_missing?(...) = record_klass.respond_to?(...) || super
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.1.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-20 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
@@ -39,6 +39,8 @@ files:
39
39
  - Rakefile
40
40
  - active_record-associated_object.gemspec
41
41
  - lib/active_record/associated_object.rb
42
+ - lib/active_record/associated_object/object_association.rb
43
+ - lib/active_record/associated_object/performs.rb
42
44
  - lib/active_record/associated_object/railtie.rb
43
45
  - lib/active_record/associated_object/version.rb
44
46
  homepage: https://github.com/kaspth/active_record-associated_object
@@ -63,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
65
  - !ruby/object:Gem::Version
64
66
  version: '0'
65
67
  requirements: []
66
- rubygems_version: 3.3.11
68
+ rubygems_version: 3.3.21
67
69
  signing_key:
68
70
  specification_version: 4
69
71
  summary: Associate a Ruby PORO with an Active Record class and have it quack like