rails_claude_skills 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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
  5. data/.github/ISSUE_TEMPLATE/question.yml +90 -0
  6. data/.github/dependabot.yml +19 -0
  7. data/.github/workflows/ci.yml +77 -0
  8. data/.github/workflows/release.yml +66 -0
  9. data/.rubocop.yml +52 -0
  10. data/CHANGELOG.md +94 -0
  11. data/CLAUDE.md +332 -0
  12. data/CODE_OF_CONDUCT.md +134 -0
  13. data/CONTRIBUTING.md +580 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +544 -0
  16. data/Rakefile +8 -0
  17. data/lib/generators/claude/agent/agent_generator.rb +71 -0
  18. data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
  19. data/lib/generators/claude/command/command_generator.rb +50 -0
  20. data/lib/generators/claude/command/templates/command.md.tt +28 -0
  21. data/lib/generators/claude/commands_library/create-pr.md +27 -0
  22. data/lib/generators/claude/commands_library/dbchange.md +19 -0
  23. data/lib/generators/claude/commands_library/quality.md +20 -0
  24. data/lib/generators/claude/commands_library/stimulus.md +19 -0
  25. data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
  26. data/lib/generators/claude/install/install_generator.rb +211 -0
  27. data/lib/generators/claude/install/templates/README.md.tt +59 -0
  28. data/lib/generators/claude/install/templates/USAGE +28 -0
  29. data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
  30. data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
  31. data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
  32. data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
  33. data/lib/generators/claude/rule/rule_generator.rb +175 -0
  34. data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
  35. data/lib/generators/claude/rules_library/code-style.md +37 -0
  36. data/lib/generators/claude/rules_library/database.md +47 -0
  37. data/lib/generators/claude/rules_library/hotwire.md +56 -0
  38. data/lib/generators/claude/rules_library/security.md +54 -0
  39. data/lib/generators/claude/rules_library/testing.md +47 -0
  40. data/lib/generators/claude/skill/skill_generator.rb +196 -0
  41. data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
  42. data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
  43. data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
  44. data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
  45. data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
  46. data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
  47. data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
  48. data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
  49. data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
  50. data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
  51. data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
  52. data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
  53. data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
  54. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
  55. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
  56. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
  57. data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
  58. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
  59. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
  60. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
  61. data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
  62. data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
  63. data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
  64. data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
  65. data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
  66. data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
  67. data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
  68. data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
  69. data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
  70. data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
  71. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
  72. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
  73. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
  74. data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
  75. data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
  76. data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
  77. data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
  78. data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
  79. data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
  80. data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
  81. data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
  82. data/lib/generators/claude/views/views_generator.rb +113 -0
  83. data/lib/rails_claude_skills/railtie.rb +16 -0
  84. data/lib/rails_claude_skills/version.rb +5 -0
  85. data/lib/rails_claude_skills.rb +27 -0
  86. data/sig/rails_claude_skills.rbs +4 -0
  87. metadata +199 -0
@@ -0,0 +1,523 @@
1
+ # API Pagination with Kaminari
2
+
3
+ Best practices and patterns for implementing pagination in JSON APIs using Kaminari.
4
+
5
+ ## REST API Pagination
6
+
7
+ ### Basic JSON Response
8
+
9
+ ```ruby
10
+ # app/controllers/api/v1/posts_controller.rb
11
+ module Api
12
+ module V1
13
+ class PostsController < ApplicationController
14
+ def index
15
+ @posts = Post.page(params[:page]).per(per_page)
16
+
17
+ render json: {
18
+ data: @posts.map { |post| PostSerializer.new(post).as_json },
19
+ pagination: pagination_metadata(@posts)
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def per_page
26
+ [(params[:per_page] || 20).to_i, 100].min # Max 100 per page
27
+ end
28
+
29
+ def pagination_metadata(collection)
30
+ {
31
+ current_page: collection.current_page,
32
+ next_page: collection.next_page,
33
+ prev_page: collection.prev_page,
34
+ total_pages: collection.total_pages,
35
+ total_count: collection.total_count,
36
+ per_page: collection.limit_value
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ ```
43
+
44
+ ### Response Example
45
+
46
+ ```json
47
+ {
48
+ "data": [
49
+ {
50
+ "id": 1,
51
+ "title": "First Post",
52
+ "body": "Content here..."
53
+ }
54
+ ],
55
+ "pagination": {
56
+ "current_page": 1,
57
+ "next_page": 2,
58
+ "prev_page": null,
59
+ "total_pages": 10,
60
+ "total_count": 245,
61
+ "per_page": 25
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Pagination Concern
67
+
68
+ Create a reusable concern for API pagination:
69
+
70
+ ```ruby
71
+ # app/controllers/concerns/api_paginatable.rb
72
+ module ApiPaginatable
73
+ extend ActiveSupport::Concern
74
+
75
+ included do
76
+ helper_method :pagination_meta, :pagination_links
77
+ end
78
+
79
+ def paginate(collection)
80
+ collection
81
+ .page(params[:page] || 1)
82
+ .per(per_page_param)
83
+ end
84
+
85
+ def pagination_meta(collection)
86
+ {
87
+ current_page: collection.current_page,
88
+ next_page: collection.next_page,
89
+ prev_page: collection.prev_page,
90
+ total_pages: collection.total_pages,
91
+ total_count: collection.total_count,
92
+ per_page: collection.limit_value
93
+ }
94
+ end
95
+
96
+ def pagination_links(collection)
97
+ base_url = request.base_url + request.path
98
+
99
+ {
100
+ self: build_link_url(base_url, collection.current_page),
101
+ first: build_link_url(base_url, 1),
102
+ prev: collection.prev_page ? build_link_url(base_url, collection.prev_page) : nil,
103
+ next: collection.next_page ? build_link_url(base_url, collection.next_page) : nil,
104
+ last: build_link_url(base_url, collection.total_pages)
105
+ }
106
+ end
107
+
108
+ private
109
+
110
+ def per_page_param
111
+ per = params[:per_page].to_i
112
+ per = default_per_page if per <= 0
113
+ [per, max_per_page].min
114
+ end
115
+
116
+ def default_per_page
117
+ 20
118
+ end
119
+
120
+ def max_per_page
121
+ 100
122
+ end
123
+
124
+ def build_link_url(base_url, page)
125
+ query_params = request.query_parameters.merge(page: page, per_page: per_page_param)
126
+ "#{base_url}?#{query_params.to_query}"
127
+ end
128
+ end
129
+ ```
130
+
131
+ ### Using the Concern
132
+
133
+ ```ruby
134
+ # app/controllers/api/v1/posts_controller.rb
135
+ module Api
136
+ module V1
137
+ class PostsController < ApplicationController
138
+ include ApiPaginatable
139
+
140
+ def index
141
+ @posts = paginate(Post.order(:created_at))
142
+
143
+ render json: {
144
+ data: @posts,
145
+ meta: pagination_meta(@posts),
146
+ links: pagination_links(@posts)
147
+ }
148
+ end
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ ## Link Header Pagination (GitHub Style)
155
+
156
+ ```ruby
157
+ # app/controllers/concerns/link_header_pagination.rb
158
+ module LinkHeaderPagination
159
+ extend ActiveSupport::Concern
160
+
161
+ included do
162
+ after_action :set_pagination_headers, only: [:index]
163
+ end
164
+
165
+ def set_pagination_headers
166
+ return unless @paginated_collection
167
+
168
+ links = []
169
+ links << build_link_header('first', 1)
170
+ links << build_link_header('prev', @paginated_collection.prev_page) if @paginated_collection.prev_page
171
+ links << build_link_header('next', @paginated_collection.next_page) if @paginated_collection.next_page
172
+ links << build_link_header('last', @paginated_collection.total_pages)
173
+
174
+ response.headers['Link'] = links.join(', ')
175
+ response.headers['X-Total-Count'] = @paginated_collection.total_count.to_s
176
+ response.headers['X-Total-Pages'] = @paginated_collection.total_pages.to_s
177
+ response.headers['X-Per-Page'] = @paginated_collection.limit_value.to_s
178
+ response.headers['X-Page'] = @paginated_collection.current_page.to_s
179
+ end
180
+
181
+ private
182
+
183
+ def build_link_header(rel, page)
184
+ url = url_for(request.query_parameters.merge(page: page, only_path: false))
185
+ %(<#{url}>; rel="#{rel}")
186
+ end
187
+ end
188
+
189
+ # app/controllers/api/v1/posts_controller.rb
190
+ module Api
191
+ module V1
192
+ class PostsController < ApplicationController
193
+ include LinkHeaderPagination
194
+
195
+ def index
196
+ @paginated_collection = Post.page(params[:page]).per(20)
197
+ render json: @paginated_collection
198
+ end
199
+ end
200
+ end
201
+ end
202
+ ```
203
+
204
+ ### Example Response Headers
205
+
206
+ ```
207
+ Link: <http://api.example.com/posts?page=1>; rel="first",
208
+ <http://api.example.com/posts?page=2>; rel="prev",
209
+ <http://api.example.com/posts?page=4>; rel="next",
210
+ <http://api.example.com/posts?page=10>; rel="last"
211
+ X-Total-Count: 245
212
+ X-Total-Pages: 10
213
+ X-Per-Page: 25
214
+ X-Page: 3
215
+ ```
216
+
217
+ ## Cursor-Based Pagination
218
+
219
+ For real-time feeds and large datasets:
220
+
221
+ ```ruby
222
+ # app/controllers/api/v1/posts_controller.rb
223
+ module Api
224
+ module V1
225
+ class PostsController < ApplicationController
226
+ def index
227
+ @posts = fetch_posts
228
+
229
+ render json: {
230
+ data: @posts,
231
+ meta: cursor_meta(@posts)
232
+ }
233
+ end
234
+
235
+ private
236
+
237
+ def fetch_posts
238
+ posts = Post.order(id: :desc)
239
+ posts = posts.where('id < ?', params[:before]) if params[:before]
240
+ posts = posts.where('id > ?', params[:after]) if params[:after]
241
+ posts.limit(per_page + 1)
242
+ end
243
+
244
+ def cursor_meta(posts)
245
+ has_more = posts.size > per_page
246
+ posts = posts.first(per_page) if has_more
247
+
248
+ {
249
+ before: posts.first&.id,
250
+ after: posts.last&.id,
251
+ has_more: has_more
252
+ }
253
+ end
254
+
255
+ def per_page
256
+ 20
257
+ end
258
+ end
259
+ end
260
+ end
261
+ ```
262
+
263
+ ## JSON:API Specification
264
+
265
+ Following JSON:API pagination spec:
266
+
267
+ ```ruby
268
+ # app/controllers/api/v1/posts_controller.rb
269
+ module Api
270
+ module V1
271
+ class PostsController < ApplicationController
272
+ def index
273
+ @posts = Post.page(params[:page][:number]).per(params[:page][:size] || 20)
274
+
275
+ render json: {
276
+ data: @posts.map { |post| PostSerializer.new(post).as_json },
277
+ links: jsonapi_pagination_links(@posts),
278
+ meta: {
279
+ total: @posts.total_count
280
+ }
281
+ }
282
+ end
283
+
284
+ private
285
+
286
+ def jsonapi_pagination_links(collection)
287
+ base_url = request.base_url + request.path
288
+
289
+ {
290
+ self: build_jsonapi_url(base_url, collection.current_page),
291
+ first: build_jsonapi_url(base_url, 1),
292
+ prev: collection.prev_page ? build_jsonapi_url(base_url, collection.prev_page) : nil,
293
+ next: collection.next_page ? build_jsonapi_url(base_url, collection.next_page) : nil,
294
+ last: build_jsonapi_url(base_url, collection.total_pages)
295
+ }
296
+ end
297
+
298
+ def build_jsonapi_url(base_url, page_number)
299
+ "#{base_url}?page[number]=#{page_number}&page[size]=#{params.dig(:page, :size) || 20}"
300
+ end
301
+ end
302
+ end
303
+ end
304
+ ```
305
+
306
+ ## GraphQL Pagination
307
+
308
+ ```ruby
309
+ # app/graphql/types/query_type.rb
310
+ module Types
311
+ class QueryType < Types::BaseObject
312
+ field :posts, Types::PostType.connection_type, null: false do
313
+ argument :page, Integer, required: false, default_value: 1
314
+ argument :per_page, Integer, required: false, default_value: 20
315
+ end
316
+
317
+ def posts(page:, per_page:)
318
+ Post.page(page).per([per_page, 100].min)
319
+ end
320
+ end
321
+ end
322
+ ```
323
+
324
+ ## Performance Optimization for APIs
325
+
326
+ ### Without Count for Large Datasets
327
+
328
+ ```ruby
329
+ # app/controllers/api/v1/posts_controller.rb
330
+ def index
331
+ @posts = Post.page(params[:page]).per(20).without_count
332
+
333
+ render json: {
334
+ data: @posts,
335
+ pagination: {
336
+ current_page: @posts.current_page,
337
+ next_page: @posts.next_page,
338
+ prev_page: @posts.prev_page,
339
+ per_page: @posts.limit_value
340
+ # total_pages and total_count not available
341
+ }
342
+ }
343
+ end
344
+ ```
345
+
346
+ ### Conditional Count Query
347
+
348
+ ```ruby
349
+ # Only run count query on first page
350
+ def index
351
+ @posts = Post.page(params[:page]).per(20)
352
+ @posts = @posts.without_count unless params[:page].to_i == 1
353
+
354
+ meta = {
355
+ current_page: @posts.current_page,
356
+ per_page: @posts.limit_value
357
+ }
358
+
359
+ # Only include total counts on first page
360
+ if params[:page].to_i == 1
361
+ meta[:total_pages] = @posts.total_pages
362
+ meta[:total_count] = @posts.total_count
363
+ end
364
+
365
+ render json: {
366
+ data: @posts,
367
+ pagination: meta
368
+ }
369
+ end
370
+ ```
371
+
372
+ ### Caching Total Count
373
+
374
+ ```ruby
375
+ # app/controllers/api/v1/posts_controller.rb
376
+ def index
377
+ @posts = Post.page(params[:page]).per(20)
378
+
379
+ # Cache the total count
380
+ total_count = Rails.cache.fetch(['posts', 'total_count'], expires_in: 5.minutes) do
381
+ Post.count
382
+ end
383
+
384
+ render json: {
385
+ data: @posts,
386
+ pagination: {
387
+ current_page: @posts.current_page,
388
+ total_count: total_count,
389
+ total_pages: (total_count.to_f / 20).ceil
390
+ }
391
+ }
392
+ end
393
+ ```
394
+
395
+ ## Filtering with Pagination
396
+
397
+ ```ruby
398
+ # app/controllers/api/v1/posts_controller.rb
399
+ def index
400
+ @posts = Post.all
401
+ @posts = apply_filters(@posts)
402
+ @posts = @posts.page(params[:page]).per(per_page)
403
+
404
+ render json: {
405
+ data: @posts,
406
+ pagination: pagination_meta(@posts),
407
+ links: pagination_links_with_filters(@posts)
408
+ }
409
+ end
410
+
411
+ private
412
+
413
+ def apply_filters(scope)
414
+ scope = scope.where(category: params[:category]) if params[:category]
415
+ scope = scope.where(status: params[:status]) if params[:status]
416
+ scope = scope.where('created_at > ?', params[:since]) if params[:since]
417
+ scope
418
+ end
419
+
420
+ def pagination_links_with_filters(collection)
421
+ base_url = request.base_url + request.path
422
+ filter_params = request.query_parameters.except(:page, :per_page)
423
+
424
+ {
425
+ self: build_filtered_url(base_url, collection.current_page, filter_params),
426
+ first: build_filtered_url(base_url, 1, filter_params),
427
+ prev: collection.prev_page ? build_filtered_url(base_url, collection.prev_page, filter_params) : nil,
428
+ next: collection.next_page ? build_filtered_url(base_url, collection.next_page, filter_params) : nil,
429
+ last: build_filtered_url(base_url, collection.total_pages, filter_params)
430
+ }
431
+ end
432
+
433
+ def build_filtered_url(base_url, page, filters)
434
+ query = filters.merge(page: page, per_page: per_page)
435
+ "#{base_url}?#{query.to_query}"
436
+ end
437
+ ```
438
+
439
+ ## Testing API Pagination
440
+
441
+ ```ruby
442
+ # spec/requests/api/v1/posts_spec.rb
443
+ require 'rails_helper'
444
+
445
+ RSpec.describe 'Api::V1::Posts', type: :request do
446
+ let!(:posts) { create_list(:post, 30) }
447
+ let(:headers) { { 'Content-Type' => 'application/json' } }
448
+
449
+ describe 'GET /api/v1/posts' do
450
+ it 'returns paginated posts' do
451
+ get '/api/v1/posts', params: { page: 1, per_page: 10 }, headers: headers
452
+
453
+ json = JSON.parse(response.body)
454
+
455
+ expect(response).to have_http_status(:ok)
456
+ expect(json['data'].size).to eq(10)
457
+ expect(json['pagination']['current_page']).to eq(1)
458
+ expect(json['pagination']['total_pages']).to eq(3)
459
+ expect(json['pagination']['total_count']).to eq(30)
460
+ end
461
+
462
+ it 'returns second page' do
463
+ get '/api/v1/posts', params: { page: 2, per_page: 10 }, headers: headers
464
+
465
+ json = JSON.parse(response.body)
466
+
467
+ expect(json['data'].size).to eq(10)
468
+ expect(json['pagination']['current_page']).to eq(2)
469
+ expect(json['pagination']['next_page']).to eq(3)
470
+ expect(json['pagination']['prev_page']).to eq(1)
471
+ end
472
+
473
+ it 'returns empty array for out of range page' do
474
+ get '/api/v1/posts', params: { page: 999 }, headers: headers
475
+
476
+ json = JSON.parse(response.body)
477
+
478
+ expect(json['data']).to be_empty
479
+ end
480
+
481
+ it 'respects per_page parameter' do
482
+ get '/api/v1/posts', params: { per_page: 5 }, headers: headers
483
+
484
+ json = JSON.parse(response.body)
485
+
486
+ expect(json['data'].size).to eq(5)
487
+ expect(json['pagination']['per_page']).to eq(5)
488
+ end
489
+
490
+ it 'enforces maximum per_page' do
491
+ get '/api/v1/posts', params: { per_page: 999 }, headers: headers
492
+
493
+ json = JSON.parse(response.body)
494
+
495
+ expect(json['pagination']['per_page']).to eq(100) # Max limit
496
+ end
497
+
498
+ it 'includes pagination links' do
499
+ get '/api/v1/posts', params: { page: 2 }, headers: headers
500
+
501
+ json = JSON.parse(response.body)
502
+
503
+ expect(json['links']['first']).to be_present
504
+ expect(json['links']['prev']).to be_present
505
+ expect(json['links']['next']).to be_present
506
+ expect(json['links']['last']).to be_present
507
+ end
508
+ end
509
+ end
510
+ ```
511
+
512
+ ## Best Practices
513
+
514
+ 1. **Always set max per_page**: Prevent abuse with reasonable limits (e.g., 100)
515
+ 2. **Include pagination metadata**: Help clients understand available data
516
+ 3. **Provide navigation links**: Make it easy to traverse pages
517
+ 4. **Use Link headers for simple APIs**: Keep response body clean
518
+ 5. **Cache total counts**: Expensive on large tables
519
+ 6. **Consider cursor pagination**: Better for real-time feeds
520
+ 7. **Document pagination**: Clear API docs with examples
521
+ 8. **Handle out of range gracefully**: Return empty array, not errors
522
+ 9. **Preserve query parameters**: Keep filters/sorts in pagination links
523
+ 10. **Use consistent format**: Pick one pagination style and stick with it