polar-ruby 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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +50 -0
  4. data/DEVELOPMENT.md +329 -0
  5. data/EXAMPLES.md +385 -0
  6. data/Gemfile +12 -0
  7. data/Gemfile.lock +115 -0
  8. data/LICENSE +23 -0
  9. data/PROJECT_SUMMARY.md +256 -0
  10. data/README.md +635 -0
  11. data/Rakefile +24 -0
  12. data/examples/demo.rb +106 -0
  13. data/lib/polar/authentication.rb +83 -0
  14. data/lib/polar/client.rb +144 -0
  15. data/lib/polar/configuration.rb +46 -0
  16. data/lib/polar/customer_portal/benefit_grants.rb +41 -0
  17. data/lib/polar/customer_portal/customers.rb +69 -0
  18. data/lib/polar/customer_portal/license_keys.rb +70 -0
  19. data/lib/polar/customer_portal/orders.rb +82 -0
  20. data/lib/polar/customer_portal/subscriptions.rb +51 -0
  21. data/lib/polar/errors.rb +96 -0
  22. data/lib/polar/http_client.rb +150 -0
  23. data/lib/polar/pagination.rb +133 -0
  24. data/lib/polar/resources/base.rb +47 -0
  25. data/lib/polar/resources/benefits.rb +64 -0
  26. data/lib/polar/resources/checkouts.rb +75 -0
  27. data/lib/polar/resources/customers.rb +120 -0
  28. data/lib/polar/resources/events.rb +45 -0
  29. data/lib/polar/resources/files.rb +57 -0
  30. data/lib/polar/resources/license_keys.rb +81 -0
  31. data/lib/polar/resources/metrics.rb +30 -0
  32. data/lib/polar/resources/oauth2.rb +61 -0
  33. data/lib/polar/resources/orders.rb +54 -0
  34. data/lib/polar/resources/organizations.rb +41 -0
  35. data/lib/polar/resources/payments.rb +29 -0
  36. data/lib/polar/resources/products.rb +58 -0
  37. data/lib/polar/resources/subscriptions.rb +55 -0
  38. data/lib/polar/resources/webhooks.rb +81 -0
  39. data/lib/polar/version.rb +5 -0
  40. data/lib/polar/webhooks.rb +174 -0
  41. data/lib/polar.rb +65 -0
  42. metadata +239 -0
data/README.md ADDED
@@ -0,0 +1,635 @@
1
+ # Polar Ruby SDK
2
+
3
+ > **Note:** This SDK was generated with the help of AI and is **not an official SDK from Polar.sh**. It is a community project and not affiliated with or endorsed by Polar.sh. Contributions, suggestions, and improvements are very welcome!
4
+
5
+ A comprehensive Ruby SDK for [Polar.sh](https://polar.sh), providing easy integration with their payment infrastructure, subscription management, and merchant services.
6
+
7
+ [![Gem Version](https://badge.fury.io/rb/polar-ruby.svg)](https://badge.fury.io/rb/polar-ruby)
8
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7.0-ruby.svg)](https://www.ruby-lang.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+
11
+ ## Features
12
+
13
+ - **Complete API Coverage**: Access all Polar.sh API endpoints
14
+ - **Type Safety**: Well-structured response objects and error handling
15
+ - **Authentication**: Support for Organization Access Tokens and Customer Sessions
16
+ - **Pagination**: Built-in pagination support for list endpoints
17
+ - **Retry Logic**: Automatic retries with exponential backoff
18
+ - **Webhook Verification**: Secure webhook signature validation
19
+ - **Environment Support**: Production and sandbox environments
20
+ - **Customer Portal**: Dedicated customer-facing API endpoints
21
+ - **Ruby Compatibility**: Supports Ruby 2.7+
22
+
23
+ ## Table of Contents
24
+
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [Authentication](#authentication)
28
+ - [Configuration](#configuration)
29
+ - [Core API Resources](#core-api-resources)
30
+ - [Customer Portal API](#customer-portal-api)
31
+ - [Pagination](#pagination)
32
+ - [Error Handling](#error-handling)
33
+ - [Webhook Verification](#webhook-verification)
34
+ - [Environment Support](#environment-support)
35
+ - [Examples](#examples)
36
+ - [Development](#development)
37
+ - [Contributing](#contributing)
38
+
39
+ ## Installation
40
+
41
+ Add this line to your application's Gemfile:
42
+
43
+ ```ruby
44
+ gem 'polar-ruby'
45
+ ```
46
+
47
+ And then execute:
48
+
49
+ ```bash
50
+ bundle install
51
+ ```
52
+
53
+ Or install it yourself as:
54
+
55
+ ```bash
56
+ gem install polar-ruby
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ```ruby
62
+ require 'polar'
63
+
64
+ # Initialize client with Organization Access Token
65
+ client = Polar.new(access_token: 'polar_oat_your_token_here')
66
+
67
+ # List organizations
68
+ organizations = client.organizations.list.auto_paginate
69
+ puts organizations.first
70
+
71
+ # Create a product
72
+ product = client.products.create({
73
+ name: "Premium Plan",
74
+ description: "Access to premium features",
75
+ organization_id: "org_123"
76
+ })
77
+
78
+ # List customers with pagination
79
+ client.customers.list.each do |customer|
80
+ puts "Customer: #{customer['name']} (#{customer['email']})"
81
+ end
82
+ ```
83
+
84
+ ## Authentication
85
+
86
+ ### Organization Access Tokens (OAT)
87
+
88
+ Use an OAT to act on behalf of your organization. Create tokens in your organization settings.
89
+
90
+ ```ruby
91
+ # Via initialization
92
+ client = Polar.new(access_token: 'polar_oat_your_token_here')
93
+
94
+ # Via environment variable
95
+ ENV['POLAR_ACCESS_TOKEN'] = 'polar_oat_your_token_here'
96
+ client = Polar.new
97
+
98
+ # Via global configuration
99
+ Polar.configure do |config|
100
+ config.access_token = 'polar_oat_your_token_here'
101
+ end
102
+ client = Polar.new
103
+ ```
104
+
105
+ ### Customer Sessions
106
+
107
+ For customer-facing operations, use customer session tokens:
108
+
109
+ ```ruby
110
+ # Customer portal operations require customer session
111
+ customer_client = Polar.new(customer_session: 'customer_session_token')
112
+
113
+ # Or pass session token to individual calls
114
+ orders = client.customer_portal.orders.list(customer_session: 'customer_session_token')
115
+ ```
116
+
117
+ ## Configuration
118
+
119
+ ### Global Configuration
120
+
121
+ ```ruby
122
+ Polar.configure do |config|
123
+ config.access_token = 'polar_oat_your_token_here'
124
+ config.server = :sandbox # :production (default) or :sandbox
125
+ config.timeout = 30 # Request timeout in seconds
126
+ config.retries = 3 # Number of retry attempts
127
+ config.logger = Logger.new(STDOUT)
128
+ config.debug = true # Enable debug logging
129
+ end
130
+ ```
131
+
132
+ ### Per-Client Configuration
133
+
134
+ ```ruby
135
+ client = Polar.new(
136
+ access_token: 'polar_oat_your_token_here',
137
+ server: :sandbox,
138
+ timeout: 60,
139
+ retries: 5,
140
+ debug: true
141
+ )
142
+ ```
143
+
144
+ ### Environment Variables
145
+
146
+ The SDK respects the following environment variables:
147
+
148
+ - `POLAR_ACCESS_TOKEN`: Default access token
149
+ - `POLAR_DEBUG`: Enable debug logging (set to 'true')
150
+
151
+ ## Core API Resources
152
+
153
+ ### Organizations
154
+
155
+ ```ruby
156
+ # List organizations
157
+ organizations = client.organizations.list.auto_paginate
158
+
159
+ # Get organization
160
+ org = client.organizations.get('org_123')
161
+
162
+ # Create organization
163
+ new_org = client.organizations.create({
164
+ name: "My Company",
165
+ slug: "my-company"
166
+ })
167
+
168
+ # Update organization
169
+ updated_org = client.organizations.update('org_123', { name: "Updated Name" })
170
+ ```
171
+
172
+ ### Products
173
+
174
+ ```ruby
175
+ # List products
176
+ products = client.products.list(organization_id: 'org_123').auto_paginate
177
+
178
+ # Create product
179
+ product = client.products.create({
180
+ name: "Premium Plan",
181
+ description: "Access to premium features",
182
+ organization_id: "org_123",
183
+ prices: [
184
+ {
185
+ type: "recurring",
186
+ amount: 2000, # $20.00 in cents
187
+ currency: "USD",
188
+ recurring: { interval: "month" }
189
+ }
190
+ ]
191
+ })
192
+
193
+ # Get product
194
+ product = client.products.get('prod_123')
195
+
196
+ # Update product
197
+ updated_product = client.products.update('prod_123', { name: "New Name" })
198
+ ```
199
+
200
+ ### Customers
201
+
202
+ ```ruby
203
+ # List customers
204
+ customers = client.customers.list(organization_id: 'org_123')
205
+
206
+ # Create customer
207
+ customer = client.customers.create({
208
+ email: "customer@example.com",
209
+ name: "John Doe",
210
+ organization_id: "org_123"
211
+ })
212
+
213
+ # Get customer
214
+ customer = client.customers.get('cust_123')
215
+
216
+ # Update customer
217
+ updated_customer = client.customers.update('cust_123', { name: "Jane Doe" })
218
+
219
+ # Get customer by external ID
220
+ customer = client.customers.get_external('external_123', organization_id: 'org_123')
221
+ ```
222
+
223
+ ### Orders
224
+
225
+ ```ruby
226
+ # List orders
227
+ orders = client.orders.list(organization_id: 'org_123')
228
+
229
+ # Get order
230
+ order = client.orders.get('order_123')
231
+
232
+ # Update order
233
+ updated_order = client.orders.update('order_123', { metadata: { key: "value" } })
234
+
235
+ # Generate invoice
236
+ invoice = client.orders.generate_invoice('order_123')
237
+ ```
238
+
239
+ ### Payments
240
+
241
+ ```ruby
242
+ # List payments
243
+ payments = client.payments.list(organization_id: 'org_123')
244
+
245
+ # Get payment
246
+ payment = client.payments.get('pay_123')
247
+ ```
248
+
249
+ ### Subscriptions
250
+
251
+ ```ruby
252
+ # List subscriptions
253
+ subscriptions = client.subscriptions.list(organization_id: 'org_123')
254
+
255
+ # Get subscription
256
+ subscription = client.subscriptions.get('sub_123')
257
+
258
+ # Update subscription
259
+ updated_sub = client.subscriptions.update('sub_123', { metadata: { key: "value" } })
260
+
261
+ # Cancel subscription
262
+ cancelled_sub = client.subscriptions.revoke('sub_123')
263
+ ```
264
+
265
+ ### Checkouts
266
+
267
+ ```ruby
268
+ # Create checkout session
269
+ checkout = client.checkouts.create({
270
+ product_price_id: "price_123",
271
+ success_url: "https://yoursite.com/success",
272
+ cancel_url: "https://yoursite.com/cancel",
273
+ customer_data: {
274
+ email: "customer@example.com"
275
+ }
276
+ })
277
+
278
+ # Get checkout session
279
+ checkout = client.checkouts.get('checkout_123')
280
+
281
+ # Client-side operations (no auth required)
282
+ checkout = client.checkouts.client_get('checkout_123')
283
+ updated_checkout = client.checkouts.client_update('checkout_123', { customer_data: { name: "John" } })
284
+ confirmed_checkout = client.checkouts.client_confirm('checkout_123')
285
+ ```
286
+
287
+ ## Customer Portal API
288
+
289
+ The Customer Portal API provides customer-facing endpoints with proper scoping:
290
+
291
+ ```ruby
292
+ # Initialize with customer session
293
+ customer_client = Polar.new(customer_session: 'customer_session_token')
294
+
295
+ # Or pass session to individual calls
296
+ session_token = 'customer_session_token'
297
+
298
+ # Get customer information
299
+ customer = client.customer_portal.customers.get(customer_session: session_token)
300
+
301
+ # List customer orders
302
+ orders = client.customer_portal.orders.list(customer_session: session_token)
303
+
304
+ # Get specific order
305
+ order = client.customer_portal.orders.get('order_123', customer_session: session_token)
306
+
307
+ # List customer subscriptions
308
+ subscriptions = client.customer_portal.subscriptions.list(customer_session: session_token)
309
+
310
+ # Cancel subscription
311
+ client.customer_portal.subscriptions.cancel('sub_123', customer_session: session_token)
312
+
313
+ # List license keys
314
+ license_keys = client.customer_portal.license_keys.list(customer_session: session_token)
315
+
316
+ # Validate license key (no auth required)
317
+ validation = client.customer_portal.license_keys.validate('license_key_123')
318
+
319
+ # Activate license key
320
+ activation = client.customer_portal.license_keys.activate('license_key_123', {
321
+ label: "Development Machine"
322
+ })
323
+ ```
324
+
325
+ ## Pagination
326
+
327
+ The SDK provides automatic pagination support:
328
+
329
+ ```ruby
330
+ # Auto-paginate (loads all pages into memory)
331
+ all_customers = client.customers.list.auto_paginate
332
+
333
+ # Manual pagination
334
+ customers_paginated = client.customers.list(organization_id: 'org_123')
335
+
336
+ # Iterate through all pages
337
+ customers_paginated.each do |customer|
338
+ puts "Customer: #{customer['name']}"
339
+ end
340
+
341
+ # Get specific page
342
+ page_2 = customers_paginated.page(2)
343
+
344
+ # Check pagination info
345
+ puts "Total: #{customers_paginated.count}"
346
+ puts "Pages: #{customers_paginated.total_pages}"
347
+ ```
348
+
349
+ ## Error Handling
350
+
351
+ The SDK provides comprehensive error handling:
352
+
353
+ ```ruby
354
+ begin
355
+ customer = client.customers.get('invalid_id')
356
+ rescue Polar::NotFoundError => e
357
+ puts "Customer not found: #{e.message}"
358
+ rescue Polar::UnauthorizedError => e
359
+ puts "Authentication failed: #{e.message}"
360
+ rescue Polar::ValidationError => e
361
+ puts "Validation error: #{e.message}"
362
+ puts "Status: #{e.status_code}"
363
+ puts "Body: #{e.body}"
364
+ rescue Polar::HTTPError => e
365
+ puts "HTTP error: #{e.status_code} - #{e.message}"
366
+ rescue Polar::ConnectionError => e
367
+ puts "Connection error: #{e.message}"
368
+ rescue Polar::TimeoutError => e
369
+ puts "Request timed out: #{e.message}"
370
+ rescue Polar::Error => e
371
+ puts "Polar SDK error: #{e.message}"
372
+ end
373
+ ```
374
+
375
+ ### Error Types
376
+
377
+ - `Polar::HTTPError` - Base HTTP error class
378
+ - `Polar::BadRequestError` - 400 Bad Request
379
+ - `Polar::UnauthorizedError` - 401 Unauthorized
380
+ - `Polar::ForbiddenError` - 403 Forbidden
381
+ - `Polar::NotFoundError` - 404 Not Found
382
+ - `Polar::UnprocessableEntityError` - 422 Validation Error
383
+ - `Polar::TooManyRequestsError` - 429 Rate Limited
384
+ - `Polar::InternalServerError` - 500 Server Error
385
+ - `Polar::ConnectionError` - Network connection failed
386
+ - `Polar::TimeoutError` - Request timeout
387
+ - `Polar::WebhookVerificationError` - Webhook signature verification failed
388
+
389
+ ## Webhook Verification
390
+
391
+ Verify webhook signatures to ensure requests are from Polar:
392
+
393
+ ```ruby
394
+ # In your webhook endpoint (Rails example)
395
+ class WebhooksController < ApplicationController
396
+ skip_before_action :verify_authenticity_token
397
+
398
+ def polar_webhook
399
+ payload = request.body.read
400
+ headers = request.headers
401
+ secret = ENV['POLAR_WEBHOOK_SECRET']
402
+
403
+ begin
404
+ event = Polar::Webhooks.validate_event(payload, headers, secret)
405
+
406
+ # Process the event
407
+ case event['type']
408
+ when 'order.created'
409
+ handle_order_created(event['data'])
410
+ when 'subscription.cancelled'
411
+ handle_subscription_cancelled(event['data'])
412
+ end
413
+
414
+ render json: { status: 'ok' }
415
+ rescue Polar::WebhookVerificationError => e
416
+ render json: { error: 'Invalid signature' }, status: 403
417
+ end
418
+ end
419
+
420
+ private
421
+
422
+ def handle_order_created(order_data)
423
+ # Process order creation
424
+ puts "New order: #{order_data['id']}"
425
+ end
426
+
427
+ def handle_subscription_cancelled(subscription_data)
428
+ # Process subscription cancellation
429
+ puts "Cancelled subscription: #{subscription_data['id']}"
430
+ end
431
+ end
432
+ ```
433
+
434
+ ### Manual Webhook Verification
435
+
436
+ ```ruby
437
+ # Verify signature manually
438
+ valid = Polar::Webhooks.verify_signature(
439
+ payload,
440
+ timestamp,
441
+ signature,
442
+ secret
443
+ )
444
+
445
+ # Parse event type
446
+ event_type = Polar::Webhooks.parse_event_type(payload)
447
+
448
+ # Check specific event type
449
+ is_order_event = Polar::Webhooks.event_type?(payload, 'order.created')
450
+
451
+ # Create event wrapper
452
+ event = Polar::Webhooks::Event.new(payload)
453
+ puts "Event ID: #{event.id}"
454
+ puts "Event Type: #{event.type}"
455
+ puts "Is subscription event: #{event.subscription_event?}"
456
+ ```
457
+
458
+ ## Environment Support
459
+
460
+ ### Production Environment
461
+
462
+ ```ruby
463
+ # Default environment
464
+ client = Polar.new(access_token: 'polar_oat_prod_token')
465
+
466
+ # Explicit production
467
+ client = Polar.new(
468
+ access_token: 'polar_oat_prod_token',
469
+ server: :production
470
+ )
471
+ ```
472
+
473
+ ### Sandbox Environment
474
+
475
+ ```ruby
476
+ # Sandbox environment for testing
477
+ client = Polar.new(
478
+ access_token: 'polar_oat_sandbox_token',
479
+ server: :sandbox
480
+ )
481
+
482
+ # Custom base URL
483
+ client = Polar.new(
484
+ access_token: 'polar_oat_token',
485
+ base_url: 'https://custom-api.polar.sh/v1'
486
+ )
487
+ ```
488
+
489
+ ## Examples
490
+
491
+ ### E-commerce Integration
492
+
493
+ ```ruby
494
+ class PolarService
495
+ def initialize
496
+ @client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
497
+ end
498
+
499
+ def create_checkout_for_product(product_id, customer_email, success_url, cancel_url)
500
+ @client.checkouts.create({
501
+ product_price_id: product_id,
502
+ success_url: success_url,
503
+ cancel_url: cancel_url,
504
+ customer_data: {
505
+ email: customer_email
506
+ }
507
+ })
508
+ end
509
+
510
+ def get_customer_orders(customer_id)
511
+ @client.orders.list(customer_id: customer_id).auto_paginate
512
+ end
513
+
514
+ def create_subscription_checkout(price_id, customer_data)
515
+ @client.checkouts.create({
516
+ product_price_id: price_id,
517
+ success_url: "#{ENV['APP_URL']}/subscription/success",
518
+ cancel_url: "#{ENV['APP_URL']}/subscription/cancel",
519
+ customer_data: customer_data
520
+ })
521
+ end
522
+ end
523
+ ```
524
+
525
+ ### License Key Management
526
+
527
+ ```ruby
528
+ class LicenseManager
529
+ def initialize
530
+ @client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
531
+ end
532
+
533
+ def validate_license(license_key)
534
+ result = @client.customer_portal.license_keys.validate(license_key)
535
+ {
536
+ valid: result['valid'],
537
+ customer: result['customer'],
538
+ product: result['product']
539
+ }
540
+ rescue Polar::Error => e
541
+ { valid: false, error: e.message }
542
+ end
543
+
544
+ def activate_license(license_key, machine_info)
545
+ @client.customer_portal.license_keys.activate(license_key, {
546
+ label: machine_info[:name],
547
+ metadata: {
548
+ os: machine_info[:os],
549
+ version: machine_info[:version]
550
+ }
551
+ })
552
+ rescue Polar::Error => e
553
+ { error: e.message }
554
+ end
555
+ end
556
+ ```
557
+
558
+ ### Subscription Management
559
+
560
+ ```ruby
561
+ class SubscriptionManager
562
+ def initialize
563
+ @client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
564
+ end
565
+
566
+ def get_customer_subscriptions(customer_session)
567
+ @client.customer_portal.subscriptions.list(
568
+ customer_session: customer_session
569
+ ).auto_paginate
570
+ end
571
+
572
+ def cancel_subscription(subscription_id, customer_session)
573
+ @client.customer_portal.subscriptions.cancel(
574
+ subscription_id,
575
+ customer_session: customer_session
576
+ )
577
+ end
578
+
579
+ def update_subscription(subscription_id, updates, customer_session)
580
+ @client.customer_portal.subscriptions.update(
581
+ subscription_id,
582
+ updates,
583
+ customer_session: customer_session
584
+ )
585
+ end
586
+ end
587
+ ```
588
+
589
+ ## Development
590
+
591
+ 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.
592
+
593
+ 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).
594
+
595
+ ### Running Tests
596
+
597
+ ```bash
598
+ # Run all tests
599
+ bundle exec rspec
600
+
601
+ # Run with coverage
602
+ bundle exec rspec --format documentation
603
+
604
+ # Run specific test file
605
+ bundle exec rspec spec/polar/client_spec.rb
606
+ ```
607
+
608
+ ### Documentation
609
+
610
+ Generate documentation:
611
+
612
+ ```bash
613
+ bundle exec yard doc
614
+ ```
615
+
616
+ ## Contributing
617
+
618
+ Bug reports and pull requests are welcome on GitHub at https://github.com/polarsource/polar-ruby. This project is intended to be a safe, welcoming space for collaboration.
619
+
620
+ 1. Fork it
621
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
622
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
623
+ 4. Push to the branch (`git push origin my-new-feature`)
624
+ 5. Create new Pull Request
625
+
626
+ ## License
627
+
628
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
629
+
630
+ ## Support
631
+
632
+ - [Polar.sh Documentation](https://polar.sh/docs)
633
+ - [API Reference](https://polar.sh/docs/api-reference)
634
+ - [GitHub Issues](https://github.com/polarsource/polar-ruby/issues)
635
+ - [Polar Discord Community](https://discord.gg/polar)
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ desc 'Run tests with coverage'
11
+ task :test_with_coverage do
12
+ ENV['COVERAGE'] = 'true'
13
+ Rake::Task[:spec].invoke
14
+ end
15
+
16
+ desc 'Generate documentation'
17
+ task :doc do
18
+ system('yard doc')
19
+ end
20
+
21
+ desc 'Run all quality checks'
22
+ task quality: %i[rubocop spec]
23
+
24
+ task default: :quality