sidekiq-sorbet 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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.vscode/cspell.json +18 -0
  3. data/.vscode/project-words.txt +8 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +266 -0
  8. data/Rakefile +12 -0
  9. data/lib/sidekiq/sorbet/class_methods.rb +114 -0
  10. data/lib/sidekiq/sorbet/errors.rb +18 -0
  11. data/lib/sidekiq/sorbet/instance_methods.rb +89 -0
  12. data/lib/sidekiq/sorbet/version.rb +7 -0
  13. data/lib/sidekiq/sorbet.rb +33 -0
  14. data/lib/tapioca/dsl/compilers/sidekiq_sorbet.rb +124 -0
  15. data/sig/sidekiq/sorbet.rbs +6 -0
  16. data/sorbet/config +4 -0
  17. data/sorbet/rbi/annotations/.gitattributes +1 -0
  18. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  19. data/sorbet/rbi/annotations/sidekiq.rbi +104 -0
  20. data/sorbet/rbi/gems/.gitattributes +1 -0
  21. data/sorbet/rbi/gems/ast@2.4.3.rbi +586 -0
  22. data/sorbet/rbi/gems/benchmark@0.5.0.rbi +637 -0
  23. data/sorbet/rbi/gems/connection_pool@3.0.2.rbi +365 -0
  24. data/sorbet/rbi/gems/date@3.5.1.rbi +403 -0
  25. data/sorbet/rbi/gems/diff-lcs@1.6.2.rbi +1135 -0
  26. data/sorbet/rbi/gems/docile@1.4.1.rbi +377 -0
  27. data/sorbet/rbi/gems/erb@6.0.1.rbi +816 -0
  28. data/sorbet/rbi/gems/erubi@1.13.1.rbi +157 -0
  29. data/sorbet/rbi/gems/io-console@0.8.2.rbi +9 -0
  30. data/sorbet/rbi/gems/json@2.18.0.rbi +2319 -0
  31. data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
  32. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +323 -0
  33. data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
  34. data/sorbet/rbi/gems/netrc@0.11.0.rbi +177 -0
  35. data/sorbet/rbi/gems/openssl@3.3.2.rbi +4529 -0
  36. data/sorbet/rbi/gems/parallel@1.27.0.rbi +291 -0
  37. data/sorbet/rbi/gems/parser@3.3.10.0.rbi +5537 -0
  38. data/sorbet/rbi/gems/pp@0.6.3.rbi +376 -0
  39. data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +477 -0
  40. data/sorbet/rbi/gems/prism@1.6.0.rbi +42126 -0
  41. data/sorbet/rbi/gems/psych@5.3.0.rbi +2542 -0
  42. data/sorbet/rbi/gems/racc@1.8.1.rbi +168 -0
  43. data/sorbet/rbi/gems/rack@3.2.4.rbi +5101 -0
  44. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
  45. data/sorbet/rbi/gems/rake@13.3.1.rbi +3036 -0
  46. data/sorbet/rbi/gems/rbi@0.3.8.rbi +5238 -0
  47. data/sorbet/rbi/gems/rbs@4.0.0.dev.4.rbi +7897 -0
  48. data/sorbet/rbi/gems/rdoc@6.17.0.rbi +13194 -0
  49. data/sorbet/rbi/gems/redis-client@0.26.2.rbi +1445 -0
  50. data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3883 -0
  51. data/sorbet/rbi/gems/reline@0.6.3.rbi +2995 -0
  52. data/sorbet/rbi/gems/require-hooks@0.2.2.rbi +110 -0
  53. data/sorbet/rbi/gems/rexml@3.4.4.rbi +5258 -0
  54. data/sorbet/rbi/gems/rspec-core@3.13.6.rbi +11306 -0
  55. data/sorbet/rbi/gems/rspec-expectations@3.13.5.rbi +8201 -0
  56. data/sorbet/rbi/gems/rspec-mocks@3.13.7.rbi +5304 -0
  57. data/sorbet/rbi/gems/rspec-support@3.13.6.rbi +1585 -0
  58. data/sorbet/rbi/gems/rspec@3.13.2.rbi +15 -0
  59. data/sorbet/rbi/gems/rubocop-ast@1.48.0.rbi +7549 -0
  60. data/sorbet/rbi/gems/rubocop-performance@1.26.1.rbi +3368 -0
  61. data/sorbet/rbi/gems/rubocop-rake@0.7.1.rbi +328 -0
  62. data/sorbet/rbi/gems/rubocop-rspec@3.8.0.rbi +7840 -0
  63. data/sorbet/rbi/gems/rubocop@1.81.7.rbi +63929 -0
  64. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
  65. data/sorbet/rbi/gems/sidekiq@8.1.0.rbi +1889 -0
  66. data/sorbet/rbi/gems/simplecov-html@0.13.2.rbi +96 -0
  67. data/sorbet/rbi/gems/simplecov@0.22.0.rbi +2149 -0
  68. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +9 -0
  69. data/sorbet/rbi/gems/spoom@1.7.10.rbi +5878 -0
  70. data/sorbet/rbi/gems/stringio@3.1.9.rbi +9 -0
  71. data/sorbet/rbi/gems/tapioca@0.17.10.rbi +3513 -0
  72. data/sorbet/rbi/gems/thor@1.4.0.rbi +4399 -0
  73. data/sorbet/rbi/gems/tsort@0.2.0.rbi +393 -0
  74. data/sorbet/rbi/gems/unicode-display_width@3.2.0.rbi +132 -0
  75. data/sorbet/rbi/gems/unicode-emoji@4.1.0.rbi +251 -0
  76. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +430 -0
  77. data/sorbet/rbi/gems/yard@0.9.38.rbi +18425 -0
  78. data/sorbet/rbi/todo.rbi +8 -0
  79. data/sorbet/tapioca/config.yml +13 -0
  80. data/sorbet/tapioca/require.rb +9 -0
  81. metadata +152 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cf07ecd6229ca17be91443492b29af00f719d9fa26f6f6f13c8f5ef3750cd43a
4
+ data.tar.gz: f821632d9af46561d5dabd1529ee0daeefd915f6ed1182a02035dc0084bb77ec
5
+ SHA512:
6
+ metadata.gz: 95792b5c0cf3a4c603098a61342d4788a8f8da9bea4269e4fcdc411b9bc631bfa94b63a9c2f15b394eef4d1251236c74310f997bf3b29e58e42139899fea5d59
7
+ data.tar.gz: ebcedc188a5a2f7a7da792c8014daaaafdb18dc0cd944497abd4ef79692c0c0abf0e616cde526bc126dec6c18f1b0d879aeebb8443c0e9e8f222e02885890861
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
3
+ "version": "0.2",
4
+ "dictionaryDefinitions": [
5
+ {
6
+ "name": "project-words",
7
+ "path": "./project-words.txt",
8
+ "addWords": true
9
+ }
10
+ ],
11
+ "dictionaries": [
12
+ "project-words"
13
+ ],
14
+ "ignorePaths": [
15
+ ".idea",
16
+ ".vscode"
17
+ ]
18
+ }
@@ -0,0 +1,8 @@
1
+ bindir
2
+ Kodkod
3
+ kwargs
4
+ nilable
5
+ popen
6
+ readlines
7
+ rubocop
8
+ sidekiq
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-12-16
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "sidekiq-sorbet" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["678665+akodkod@users.noreply.github.com"](mailto:"678665+akodkod@users.noreply.github.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Andrew Kodkod
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # Sidekiq::Sorbet
2
+
3
+ Add typed arguments to your Sidekiq Workers with automatic argument access.
4
+
5
+ ## Quick Example
6
+
7
+ ```ruby
8
+ # Worker Class
9
+ class AnalyzeAttachmentWorker
10
+ include Sidekiq::Sorbet
11
+
12
+ class Args < T::Struct
13
+ const :attachment_id, Integer
14
+ const :regenerate, T::Boolean, default: false
15
+ end
16
+
17
+ def run
18
+ # Direct access to typed arguments
19
+ attachment = Attachment.find(attachment_id)
20
+ return if attachment.analyzed? && !regenerate
21
+
22
+ attachment.analyze!
23
+ end
24
+ end
25
+
26
+ # Call Worker
27
+ AnalyzeAttachmentWorker.run_async(attachment_id: 1) # arguments are typed and validated
28
+ AnalyzeAttachmentWorker.run_sync(attachment_id: 1, regenerate: true) # execute synchronously
29
+ ```
30
+
31
+ ## Features
32
+
33
+ - ✅ **Direct argument access** - Access arguments directly as `attachment_id` instead of `args.attachment_id`
34
+ - ✅ **Type safety** - Arguments are validated at enqueue time using Sorbet's T::Struct
35
+ - ✅ **Optional Args** - Workers can omit the Args class if they don't need arguments
36
+ - ✅ **Backward compatible** - The `args` accessor still works: `args.attachment_id`
37
+ - ✅ **Clean API** - Use `run_async`/`run_sync` instead of `perform_async`/`perform`
38
+ - ✅ **Fail-fast validation** - Errors caught before jobs are enqueued
39
+ - ✅ **Comprehensive errors** - Clear error messages for debugging
40
+
41
+ ## Installation
42
+
43
+ Install the gem and add to the application's Gemfile by executing:
44
+
45
+ ```bash
46
+ bundle add sidekiq-sorbet
47
+ ```
48
+
49
+ If bundler is not being used to manage dependencies, install the gem by executing:
50
+
51
+ ```bash
52
+ gem install sidekiq-sorbet
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### Basic Worker with Arguments
58
+
59
+ ```ruby
60
+ class ProcessUserWorker
61
+ include Sidekiq::Sorbet
62
+
63
+ class Args < T::Struct
64
+ const :user_id, Integer
65
+ const :send_email, T::Boolean, default: true
66
+ end
67
+
68
+ def run
69
+ user = User.find(user_id)
70
+ user.process!
71
+ UserMailer.processed(user).deliver_later if send_email
72
+ end
73
+ end
74
+
75
+ # Enqueue the job
76
+ ProcessUserWorker.run_async(user_id: 123)
77
+ ProcessUserWorker.run_async(user_id: 456, send_email: false)
78
+ ```
79
+
80
+ ### Worker Without Arguments
81
+
82
+ Args class is optional! Workers without arguments work perfectly:
83
+
84
+ ```ruby
85
+ class CleanupWorker
86
+ include Sidekiq::Sorbet
87
+
88
+ def run
89
+ # Perform cleanup tasks
90
+ clean_temp_files
91
+ vacuum_database
92
+ end
93
+ end
94
+
95
+ # No arguments needed
96
+ CleanupWorker.run_async
97
+ ```
98
+
99
+ ### Complex Types
100
+
101
+ ```ruby
102
+ class ReportWorker
103
+ include Sidekiq::Sorbet
104
+
105
+ class Args < T::Struct
106
+ const :user_ids, T::Array[Integer]
107
+ const :filters, T::Hash[String, T.untyped], default: {}
108
+ const :format, String
109
+ end
110
+
111
+ def run
112
+ users = User.where(id: user_ids)
113
+ report = ReportGenerator.new(users, filters)
114
+ report.export(format)
115
+ end
116
+ end
117
+
118
+ ReportWorker.run_async(
119
+ user_ids: [1, 2, 3],
120
+ filters: { "active" => true },
121
+ format: "pdf",
122
+ )
123
+ ```
124
+
125
+ ### Nested T::Structs
126
+
127
+ ```ruby
128
+ class NotificationWorker
129
+ include Sidekiq::Sorbet
130
+
131
+ class Recipient < T::Struct
132
+ const :name, String
133
+ const :email, String
134
+ end
135
+
136
+ class Args < T::Struct
137
+ const :recipient, Recipient
138
+ const :message, String
139
+ end
140
+
141
+ def run
142
+ # Access nested struct fields
143
+ email = NotificationMailer.compose(
144
+ to: recipient.email,
145
+ name: recipient.name,
146
+ body: message,
147
+ )
148
+ email.deliver
149
+ end
150
+ end
151
+
152
+ NotificationWorker.run_async(
153
+ recipient: NotificationWorker::Recipient.new(
154
+ name: "John Doe",
155
+ email: "john@example.com",
156
+ ),
157
+ message: "Hello!",
158
+ )
159
+ ```
160
+
161
+ ### Backward Compatibility
162
+
163
+ The `args` accessor still works if you prefer the old style:
164
+
165
+ ```ruby
166
+ class LegacyWorker
167
+ include Sidekiq::Sorbet
168
+
169
+ class Args < T::Struct
170
+ const :value, Integer
171
+ end
172
+
173
+ def run
174
+ # Both styles work
175
+ direct = value # Direct access (recommended)
176
+ via_args = args.value # Via args accessor (still works)
177
+ end
178
+ end
179
+ ```
180
+
181
+ ### Synchronous Execution
182
+
183
+ Use `run_sync` for testing or immediate execution:
184
+
185
+ ```ruby
186
+ # In tests
187
+ result = ProcessUserWorker.run_sync(user_id: 123)
188
+
189
+ # In console for debugging
190
+ AnalyzeAttachmentWorker.run_sync(attachment_id: 456)
191
+ ```
192
+
193
+ ## Tapioca DSL Compiler
194
+
195
+ This gem includes a Tapioca DSL compiler that generates proper type signatures for your workers. This enables Sorbet to understand the dynamically generated methods like `run_async`, `run_sync`, and argument accessors.
196
+
197
+ ### Generating RBI Files
198
+
199
+ Run the Tapioca DSL compiler to generate type definitions:
200
+
201
+ ```bash
202
+ bundle exec tapioca dsl SidekiqSorbet
203
+ ```
204
+
205
+ Or generate RBI files for all DSL compilers:
206
+
207
+ ```bash
208
+ bundle exec tapioca dsl
209
+ ```
210
+
211
+ ### Generated Signatures
212
+
213
+ For a worker like this:
214
+
215
+ ```ruby
216
+ class MyWorker
217
+ include Sidekiq::Sorbet
218
+
219
+ class Args < T::Struct
220
+ const :user_id, Integer
221
+ const :notify, T::Boolean, default: false
222
+ end
223
+
224
+ def run
225
+ # ...
226
+ end
227
+ end
228
+ ```
229
+
230
+ The compiler generates:
231
+
232
+ ```rbi
233
+ class MyWorker
234
+ sig { returns(Integer) }
235
+ def user_id; end
236
+
237
+ sig { returns(T::Boolean) }
238
+ def notify; end
239
+
240
+ sig { params(user_id: Integer, notify: T::Boolean).returns(String) }
241
+ def self.run_async(user_id:, notify: T.unsafe(nil)); end
242
+
243
+ sig { params(user_id: Integer, notify: T::Boolean).returns(T.untyped) }
244
+ def self.run_sync(user_id:, notify: T.unsafe(nil)); end
245
+ end
246
+ ```
247
+
248
+ Workers without an `Args` class will still have `run_async` and `run_sync` generated with no parameters.
249
+
250
+ ## Development
251
+
252
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
253
+
254
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
255
+
256
+ ## Contributing
257
+
258
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/sidekiq-sorbet. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/sidekiq-sorbet/blob/main/CODE_OF_CONDUCT.md).
259
+
260
+ ## License
261
+
262
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
263
+
264
+ ## Code of Conduct
265
+
266
+ Everyone interacting in the Sidekiq::Sorbet project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/sidekiq-sorbet/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: [:spec, :rubocop]
@@ -0,0 +1,114 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Sidekiq
7
+ module Sorbet
8
+ # Class methods added to workers that include Sidekiq::Sorbet
9
+ module ClassMethods
10
+ extend T::Sig
11
+
12
+ # Enqueues a job asynchronously with validated arguments
13
+ #
14
+ # @param kwargs [Hash] Arguments matching the Args T::Struct (or empty if no Args)
15
+ # @return [String] Sidekiq job ID
16
+ # @raise [InvalidArgsError] if arguments fail validation
17
+ # @raise [SerializationError] if serialization fails
18
+ sig { params(kwargs: T.untyped).returns(String) }
19
+ def run_async(**kwargs)
20
+ args_instance = build_args(**kwargs)
21
+ serialized = serialize_args(args_instance)
22
+ perform_async(serialized)
23
+ end
24
+
25
+ # Executes a job synchronously with validated arguments
26
+ #
27
+ # @param kwargs [Hash] Arguments matching the Args T::Struct (or empty if no Args)
28
+ # @return [Object] Result of the run method
29
+ # @raise [InvalidArgsError] if arguments fail validation
30
+ sig { params(kwargs: T.untyped).returns(T.untyped) }
31
+ def run_sync(**kwargs)
32
+ args_instance = build_args(**kwargs)
33
+ worker = new
34
+ worker.instance_variable_set(:@args, args_instance)
35
+ worker.define_arg_accessors if args_instance
36
+ worker.run
37
+ rescue InvalidArgsError, ArgsNotDefinedError, SerializationError
38
+ raise
39
+ rescue StandardError => e
40
+ raise Error,
41
+ "Error in #{name}#run: #{e.message}\n#{e.backtrace&.join("\n")}"
42
+ end
43
+
44
+ # Returns the Args class for this worker, or nil if not defined
45
+ #
46
+ # @return [Class, nil] The Args T::Struct class or nil
47
+ sig { returns(T.nilable(T.class_of(T::Struct))) }
48
+ def args_class
49
+ return @args_class if defined?(@args_class)
50
+
51
+ @args_class = T.let(
52
+ begin
53
+ klass = const_defined?(:Args, false) ? const_get(:Args) : nil
54
+ validate_args_class!(klass) if klass
55
+ klass
56
+ end,
57
+ T.nilable(T.class_of(T::Struct)),
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ # Validates that the Args class is a T::Struct
64
+ #
65
+ # @param klass [Class] The class to validate
66
+ # @return [Class] The validated class
67
+ # @raise [ArgsNotDefinedError] if invalid
68
+ sig { params(klass: T.untyped).returns(T.class_of(T::Struct)) }
69
+ def validate_args_class!(klass)
70
+ unless klass < T::Struct
71
+ raise ArgsNotDefinedError,
72
+ "#{name}::Args must inherit from T::Struct, got #{klass.class}"
73
+ end
74
+
75
+ klass
76
+ end
77
+
78
+ # Builds and validates Args instance (or returns nil if no Args class)
79
+ #
80
+ # @param kwargs [Hash] Arguments to pass to Args.new
81
+ # @return [T::Struct, nil] Validated Args instance or nil
82
+ # @raise [InvalidArgsError] if validation fails
83
+ sig { params(kwargs: T.untyped).returns(T.nilable(T::Struct)) }
84
+ def build_args(**kwargs)
85
+ return nil unless args_class
86
+
87
+ if kwargs.empty?
88
+ # No args provided - create empty Args if possible
89
+ args_class.new
90
+ else
91
+ args_class.new(**kwargs)
92
+ end
93
+ rescue ArgumentError, TypeError => e
94
+ raise InvalidArgsError,
95
+ "Invalid arguments for #{name}: #{e.message}"
96
+ end
97
+
98
+ # Serializes Args instance to JSON-compatible hash
99
+ #
100
+ # @param args_instance [T::Struct, nil] The Args instance or nil
101
+ # @return [Hash] Serialized hash with string keys (empty if nil)
102
+ # @raise [SerializationError] if serialization fails
103
+ sig { params(args_instance: T.nilable(T::Struct)).returns(T::Hash[String, T.untyped]) }
104
+ def serialize_args(args_instance)
105
+ return {} unless args_instance
106
+
107
+ args_instance.serialize
108
+ rescue StandardError => e
109
+ raise SerializationError,
110
+ "Failed to serialize args for #{name}: #{e.message}"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,18 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Sidekiq
5
+ module Sorbet
6
+ # Base error class for all Sidekiq::Sorbet errors
7
+ class Error < StandardError; end
8
+
9
+ # Raised when a worker doesn't define an Args class or it's not a T::Struct
10
+ class ArgsNotDefinedError < Error; end
11
+
12
+ # Raised when argument validation fails at enqueue time
13
+ class InvalidArgsError < Error; end
14
+
15
+ # Raised when serialization or deserialization fails
16
+ class SerializationError < Error; end
17
+ end
18
+ end
@@ -0,0 +1,89 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Sidekiq
7
+ module Sorbet
8
+ # Instance methods added to workers that include Sidekiq::Sorbet
9
+ module InstanceMethods
10
+ extend T::Sig
11
+
12
+ # Sidekiq calls this method when executing the job
13
+ # We deserialize the args and delegate to the user's run method
14
+ #
15
+ # @param serialized_hash [Hash] Serialized arguments from Sidekiq
16
+ # @return [Object] Result of the run method
17
+ # @raise [SerializationError] if deserialization fails
18
+ sig { params(serialized_hash: T::Hash[String, T.untyped]).returns(T.untyped) }
19
+ def perform(serialized_hash)
20
+ @args = T.let(
21
+ deserialize_args(serialized_hash),
22
+ T.nilable(T::Struct),
23
+ )
24
+ define_arg_accessors if @args
25
+ run
26
+ rescue SerializationError
27
+ raise
28
+ rescue StandardError => e
29
+ raise Error,
30
+ "Error in #{self.class.name}#run: #{e.message}\n#{e.backtrace&.join("\n")}"
31
+ end
32
+
33
+ # Accessor for typed arguments (optional, for backward compatibility)
34
+ #
35
+ # @return [T::Struct, nil] The Args instance or nil
36
+ sig { returns(T.nilable(T::Struct)) }
37
+ def args
38
+ @args
39
+ end
40
+
41
+ # Define getter methods for each field in the Args struct
42
+ # This allows direct access like `attachment_id` instead of `args.attachment_id`
43
+ #
44
+ # @return [void]
45
+ sig { void }
46
+ def define_arg_accessors
47
+ return unless @args
48
+
49
+ @args.class.props.each_key do |field_name|
50
+ # Skip if method already defined (avoid overriding user methods)
51
+ next if respond_to?(field_name, true) && !@args.respond_to?(field_name)
52
+
53
+ # Define a method to access the field
54
+ define_singleton_method(field_name) do
55
+ @args.public_send(field_name)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Users implement this method in their worker
61
+ #
62
+ # @return [Object] Whatever the worker returns
63
+ # @raise [NotImplementedError] if not overridden
64
+ sig { returns(T.untyped) }
65
+ def run
66
+ raise NotImplementedError,
67
+ "#{self.class.name} must implement #run method"
68
+ end
69
+
70
+ private
71
+
72
+ # Deserializes hash back into Args T::Struct
73
+ #
74
+ # @param serialized_hash [Hash] Hash with string keys from Sidekiq
75
+ # @return [T::Struct, nil] Deserialized Args instance or nil
76
+ # @raise [SerializationError] if deserialization fails
77
+ sig { params(serialized_hash: T::Hash[String, T.untyped]).returns(T.nilable(T::Struct)) }
78
+ def deserialize_args(serialized_hash)
79
+ args_klass = self.class.args_class
80
+ return nil unless args_klass
81
+
82
+ args_klass.from_hash(serialized_hash)
83
+ rescue StandardError => e
84
+ raise SerializationError,
85
+ "Failed to deserialize args for #{self.class.name}: #{e.message}"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Sorbet
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require_relative "sorbet/version"
6
+ require_relative "sorbet/errors"
7
+ require_relative "sorbet/class_methods"
8
+ require_relative "sorbet/instance_methods"
9
+
10
+ # Load Tapioca DSL compiler if Tapioca is available
11
+ begin
12
+ require "tapioca/dsl"
13
+ require_relative "../tapioca/dsl/compilers/sidekiq_sorbet"
14
+ rescue LoadError
15
+ # Tapioca not available, skip compiler
16
+ end
17
+
18
+ module Sidekiq
19
+ module Sorbet
20
+ extend T::Sig
21
+
22
+ # Hook called when Sidekiq::Sorbet is included in a worker
23
+ # Automatically includes Sidekiq::Job if needed and wires up our modules
24
+ #
25
+ # @param base [Class] The worker class including this module
26
+ sig { params(base: T.untyped).void }
27
+ def self.included(base)
28
+ base.include(Sidekiq::Job) unless base.ancestors.include?(Sidekiq::Job)
29
+ base.extend(ClassMethods)
30
+ base.include(InstanceMethods)
31
+ end
32
+ end
33
+ end