booqable 1.0.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c084014d612e3e317590f31144911c3a3907a85075a17b3172edd2f6eb8c7d9f
4
- data.tar.gz: 23836e46596f37b74175cd45f4e6ee04ee17eb7b0254bb53395972bc801a55aa
3
+ metadata.gz: 13ee5f72ca221d2613d8691dd8c04fa189a38fbed8276b60878d2cce4c7f4f23
4
+ data.tar.gz: 54028d30a657d3651ad0d49fab923efba00e09fa9f0d2e36906e8cf068975abd
5
5
  SHA512:
6
- metadata.gz: 0a2eb78b9a78a239bec1fda211b177533102e78e4cdd95470692cb5a21132180827d2339caadf723564de9d8091d15c4e6160489f5a4694dfe40fc8bd4a630c2
7
- data.tar.gz: 7210d24c6985facb00617c0949abe55c0b9034acb53b3120039c7d78e1da3ecd763875ff54c69672e39701049a3ab46f46024c1c6511251840891163a19518c0
6
+ metadata.gz: 51cb9ed5b66956cee3e429c51057e12884574fe3c8b3b907d6998caa1b037b5ca861c929ea7b26e8364369bf424b3686feff274149fa6a169da09d8048118390
7
+ data.tar.gz: d6389d7f934088b0447dba290054456f10380578f5b38622275d6e846c99872a5060c21b192b8524aa7d867fe1b58917c9ad03eebfacef716b83eca28dab8549
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2025-06-27
3
+ ## [1.1.0] - 2026-03-11
4
+
5
+ - Add `parse_resource` method (aliased as `deserialize_resource`) for parsing
6
+ JSON:API payloads into Sawyer::Resource objects with dot-notation access.
7
+ Useful for parsing webhook payloads or raw API responses.
8
+ - Add Booqable::RefreshTokenRevoked and Booqable::InvalidGrant error types for
9
+ invalid grant OAuth response scenarios
10
+ - Add "all" as an alias for "list" method on all resources.
11
+ You can now use `Booqable.orders.all` as an alternative to `Booqable.orders.list`.
12
+
13
+ ## [1.0.0] - 2025-10-23
4
14
 
5
15
  - Initial release
data/README.md CHANGED
@@ -5,6 +5,7 @@ Ruby toolkit for the [Booqable API](https://developers.booqable.com/).
5
5
  [Booqable](https://booqable.com) is a rental management platform that helps businesses manage their rental inventory, customers, and orders. This gem provides a Ruby interface to interact with all Booqable API endpoints.
6
6
 
7
7
  ## Table of Contents
8
+
8
9
  - [Installation](#installation)
9
10
  - [Making requests](#making-requests)
10
11
  - [Authentication](#authentication)
@@ -12,6 +13,7 @@ Ruby toolkit for the [Booqable API](https://developers.booqable.com/).
12
13
  - [Pagination](#pagination)
13
14
  - [Rate limiting](#rate-limiting)
14
15
  - [Resources](#resources)
16
+ - [Parsing JSON:API payloads](#parsing-jsonapi-payloads)
15
17
  - [Advanced usage](#advanced-usage)
16
18
  - [Development](#development)
17
19
 
@@ -89,11 +91,11 @@ client = Booqable::Client.new(
89
91
  client_id: 'your_oauth_client_id',
90
92
  client_secret: 'your_oauth_client_secret',
91
93
  company_id: 'your_company_id',
92
- read_token: -> {
94
+ read_token: -> {
93
95
  # Return stored token hash
94
96
  JSON.parse(File.read('token.json'))
95
97
  },
96
- write_token: ->(token) {
98
+ write_token: ->(token) {
97
99
  # Store token hash
98
100
  File.write('token.json', token.to_json)
99
101
  }
@@ -159,7 +161,7 @@ client = Booqable::Client.new(
159
161
 
160
162
  ## Pagination
161
163
 
162
- The Booqable API uses cursor-based pagination. Booqable provides several ways to handle paginated responses:
164
+ Booqable provides several ways to handle paginated responses:
163
165
 
164
166
  ### Manual pagination
165
167
 
@@ -222,6 +224,13 @@ orders = Booqable.orders.list(
222
224
  sort: '-created_at'
223
225
  )
224
226
 
227
+ # `all` is an alias for `list`
228
+ orders = Booqable.orders.all(
229
+ include: 'customer,items',
230
+ filter: { status: 'reserved' },
231
+ sort: '-created_at'
232
+ )
233
+
225
234
  # Find specific order
226
235
  order = Booqable.orders.find('order_id', include: 'customer,items')
227
236
  order.items.count
@@ -236,7 +245,7 @@ new_order = Booqable.orders.create(
236
245
  new_order.status # => 'draft'
237
246
 
238
247
  # Update order
239
- updated_order = Booqable.orders.update('order_id', status: 'reserved')
248
+ updated_order = Booqable.orders.update('order_id', stops_at: '2024-01-03T00:00:00Z')
240
249
  updated_order.status # => 'reserved'
241
250
 
242
251
  # Delete order
@@ -296,20 +305,83 @@ deleted_product.id # => 'product_id'
296
305
  Booqable provides access to all Booqable API resources:
297
306
 
298
307
  **Core Resources:**
308
+
299
309
  - `orders`, `customers`, `products`, `items`
300
310
  - `employees`, `companies`, `locations`
301
311
  - `payments`, `invoices`, `documents`
302
312
 
303
313
  **Inventory Management:**
314
+
304
315
  - `inventory_levels`, `stock_items`, `stock_adjustments`
305
316
  - `transfers`, `plannings`, `clusters`
306
317
 
307
318
  **Configuration:**
319
+
308
320
  - `settings`, `properties`, `tax_rates`
309
321
  - `payment_methods`, `email_templates`
310
322
 
311
323
  **And many more...** See the [full resource list](lib/booqable/resources.json) for all available endpoints.
312
324
 
325
+ ## Parsing JSON:API payloads
326
+
327
+ Booqable provides a convenient way to parse JSON:API formatted data (such as webhook payloads or raw API responses) into Ruby objects with dot-notation access:
328
+
329
+ ```ruby
330
+ # Parse a webhook payload
331
+ payload = {
332
+ "data" => {
333
+ "id" => "abc-123",
334
+ "type" => "customers",
335
+ "attributes" => {
336
+ "name" => "John Doe",
337
+ "email" => "john@example.com",
338
+ "created_at" => "2024-01-15T10:30:00Z"
339
+ }
340
+ }
341
+ }
342
+
343
+ customer = Booqable.parse_resource(payload)
344
+ customer.id # => "abc-123"
345
+ customer.name # => "John Doe"
346
+ customer.email # => "john@example.com"
347
+ customer.created_at # => 2024-01-15 10:30:00 UTC (Time object)
348
+ ```
349
+
350
+ ### With nested relationships
351
+
352
+ ```ruby
353
+ payload = {
354
+ "data" => {
355
+ "id" => "order-123",
356
+ "type" => "orders",
357
+ "attributes" => { "status" => "reserved" },
358
+ "relationships" => {
359
+ "customer" => { "data" => { "id" => "customer-456", "type" => "customers" } }
360
+ }
361
+ },
362
+ "included" => [
363
+ {
364
+ "id" => "customer-456",
365
+ "type" => "customers",
366
+ "attributes" => { "name" => "Jane Smith" }
367
+ }
368
+ ]
369
+ }
370
+
371
+ order = Booqable.parse_resource(payload)
372
+ order.status # => "reserved"
373
+ order.customer.name # => "Jane Smith"
374
+ ```
375
+
376
+ ### Using with a client instance
377
+
378
+ ```ruby
379
+ # If you already have a client instance
380
+ customer = client.parse_resource(payload)
381
+ ```
382
+
383
+ `deserialize_resource` is available as an alias for `parse_resource`.
384
+
313
385
  ## Advanced usage
314
386
 
315
387
  ### Custom middleware
@@ -346,18 +418,6 @@ Booqable.configure do |c|
346
418
  end
347
419
  ```
348
420
 
349
- ### Custom serialization
350
-
351
- ```ruby
352
- # Access raw response data
353
- response = Booqable.orders.list
354
- puts response.class # => Sawyer::Resource
355
-
356
- # Get response metadata
357
- puts Booqable.last_response.status
358
- puts Booqable.last_response.headers
359
- ```
360
-
361
421
  ## Development
362
422
 
363
423
  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.
@@ -376,7 +436,7 @@ bundle exec rspec spec/booqable/client_spec.rb
376
436
 
377
437
  ### Contributing
378
438
 
379
- Bug reports and pull requests are welcome on GitHub at https://github.com/booqable/booqable.rb. 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/booqable/booqable.rb/blob/master/CODE_OF_CONDUCT.md).
439
+ Bug reports and pull requests are welcome on GitHub at https://github.com/booqable/booqable.rb. 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/booqable/booqable.rb/blob/main/CODE_OF_CONDUCT.md).
380
440
 
381
441
  ## Versioning
382
442
 
@@ -396,7 +456,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
396
456
 
397
457
  ## Code of Conduct
398
458
 
399
- Everyone interacting in the Booqable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/booqable/booqable.rb/blob/master/CODE_OF_CONDUCT.md).
459
+ Everyone interacting in the Booqable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/booqable/booqable.rb/blob/main/CODE_OF_CONDUCT.md).
400
460
 
401
461
  ---
402
462
 
@@ -56,6 +56,23 @@ module Booqable
56
56
  end
57
57
  end
58
58
 
59
+ # Parse a JSON:API payload into a Sawyer::Resource
60
+ #
61
+ # Converts JSON:API formatted data (from webhooks or API responses) into
62
+ # Ruby objects with dot-notation access for convenient attribute access.
63
+ #
64
+ # @param payload [String, Hash] JSON:API payload (string or parsed hash)
65
+ # @return [Sawyer::Resource, nil] Parsed resource object, or nil for empty input
66
+ #
67
+ # @example
68
+ # customer = client.parse_resource(webhook_payload)
69
+ # customer.name # => "John"
70
+ #
71
+ def parse_resource(payload)
72
+ Booqable::ResourceParser.parse(payload)
73
+ end
74
+ alias_method :deserialize_resource, :parse_resource
75
+
59
76
  # String representation of the client with sensitive information masked
60
77
  #
61
78
  # Overrides the default inspect method to hide sensitive configuration
@@ -42,8 +42,8 @@ module Booqable
42
42
  # headers = response[:response_headers]
43
43
 
44
44
  if klass = case status
45
- when 400 then error_for_400(body)
46
- when 401 then Booqable::Unauthorized
45
+ when 400 then error_for_400(response)
46
+ when 401 then error_for_401(response)
47
47
  when 402 then error_for_402(body)
48
48
  when 403 then Booqable::Forbidden
49
49
  when 404 then error_for_404(body)
@@ -85,8 +85,8 @@ module Booqable
85
85
  # Return most appropriate error for 400 HTTP status code
86
86
  # @private
87
87
  # rubocop:disable Metrics/CyclomaticComplexity
88
- def self.error_for_400(body)
89
- case body
88
+ def self.error_for_400(response)
89
+ case response.body
90
90
  when /unwrittable_attribute/i
91
91
  Booqable::ReadOnlyAttribute
92
92
  when /unknown_attribute/i
@@ -103,12 +103,25 @@ module Booqable
103
103
  Booqable::InvalidFilter
104
104
  when /required filter/i
105
105
  Booqable::RequiredFilter
106
+ when /invalid_grant/i
107
+ error_for_invalid_grant(response)
106
108
  else
107
109
  Booqable::BadRequest
108
110
  end
109
111
  end
110
112
  # rubocop:enable Metrics/CyclomaticComplexity
111
113
 
114
+ # Return most appropriate error for 401 HTTP status code
115
+ # @private
116
+ def self.error_for_401(response)
117
+ case response.body
118
+ when /token is invalid \(revoked\)/i
119
+ Booqable::TokenRevoked
120
+ else
121
+ Booqable::Unauthorized
122
+ end
123
+ end
124
+
112
125
  # Return most appropriate error for 402 HTTP status code
113
126
  # @private
114
127
  # rubocop:disable Metrics/CyclomaticComplexity
@@ -163,6 +176,26 @@ module Booqable
163
176
  end
164
177
  end
165
178
 
179
+ # Return most appropriate error for invalid_grant OAuth error
180
+ #
181
+ # Determines whether the invalid_grant error is due to a revoked refresh token
182
+ # or a different OAuth grant error by examining the grant_type parameter
183
+ # in the request body.
184
+ #
185
+ # @param response [Hash] HTTP response containing the request body
186
+ # @return [Class] RefreshTokenRevoked if grant_type is refresh_token, InvalidGrant otherwise
187
+ # @private
188
+ def self.error_for_invalid_grant(response)
189
+ grant_type = CGI.parse(response.request_body).dig("grant_type", 0)
190
+
191
+ case grant_type
192
+ when /refresh_token/i
193
+ Booqable::RefreshTokenRevoked
194
+ else
195
+ Booqable::InvalidGrant
196
+ end
197
+ end
198
+
166
199
  # Array of validation errors
167
200
  # @return [Array<Hash>] Error info
168
201
  def errors
@@ -300,6 +333,20 @@ module Booqable
300
333
  # and body matches 'required filter'
301
334
  class RequiredFilter < ClientError; end
302
335
 
336
+ # Raised when Booqable returns a 401 HTTP status code
337
+ # and body matches 'token is invalid (revoked)'
338
+ class TokenRevoked < ClientError; end
339
+
340
+ # Raised when Booqable returns a 400 HTTP status code
341
+ # and body matches 'invalid_grant' and
342
+ # the grant type is refresh token (OAuth error)
343
+ class RefreshTokenRevoked < TokenRevoked; end
344
+
345
+ # Raised when Booqable returns a 400 HTTP status code
346
+ # and body matches 'invalid_grant' and
347
+ # grant type is not refresh token (OAuth error)
348
+ class InvalidGrant < ClientError; end
349
+
303
350
  # Raised when Booqable returns a 401 HTTP status code
304
351
  class Unauthorized < ClientError; end
305
352
 
@@ -78,7 +78,7 @@ module Booqable
78
78
 
79
79
  new_token
80
80
  rescue OAuth2::Error => e
81
- response = e.response.response.env.to_h
81
+ response = e.response.response.env
82
82
 
83
83
  Booqable::Error.from_response(response)
84
84
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booqable
4
+ # Parses JSON:API payloads into Sawyer::Resource objects
5
+ #
6
+ # JSON:API formatted data (from webhooks or API responses) is converted
7
+ # into Ruby objects with dot-notation access for convenient attribute access.
8
+ #
9
+ # @example Parse a JSON:API payload
10
+ # payload = '{"data":{"id":"123","type":"customers","attributes":{"name":"John"}}}'
11
+ # customer = Booqable::ResourceParser.parse(payload)
12
+ # customer.id # => "123"
13
+ # customer.name # => "John"
14
+ #
15
+ # @example Parse with nested relationships
16
+ # payload = {
17
+ # "data" => {
18
+ # "id" => "123",
19
+ # "type" => "orders",
20
+ # "attributes" => { "status" => "reserved" },
21
+ # "relationships" => {
22
+ # "customer" => { "data" => { "id" => "456", "type" => "customers" } }
23
+ # }
24
+ # },
25
+ # "included" => [
26
+ # { "id" => "456", "type" => "customers", "attributes" => { "name" => "John" } }
27
+ # ]
28
+ # }
29
+ # order = Booqable::ResourceParser.parse(payload)
30
+ # order.customer.name # => "John"
31
+ #
32
+ class ResourceParser
33
+ # Parse a JSON:API payload into a Sawyer::Resource
34
+ #
35
+ # @param payload [String, Hash] JSON:API payload (string or parsed hash)
36
+ # @return [Sawyer::Resource, nil] Parsed resource object with dot-notation access,
37
+ # or nil for empty/nil input
38
+ def self.parse(payload)
39
+ new(payload).parse
40
+ end
41
+
42
+ # Initialize a new ResourceParser
43
+ #
44
+ # @param payload [String, Hash] JSON:API payload
45
+ def initialize(payload)
46
+ @payload = payload
47
+ end
48
+
49
+ # Parse the payload into a Sawyer::Resource
50
+ #
51
+ # @return [Sawyer::Resource, nil] Parsed resource or nil for empty input
52
+ def parse
53
+ return nil if @payload.nil?
54
+
55
+ json_string = @payload.is_a?(String) ? @payload : @payload.to_json
56
+ return nil if json_string.strip.empty?
57
+
58
+ serializer = JsonApiSerializer.any_json
59
+ decoded = serializer.decode(json_string)
60
+
61
+ return nil unless decoded && decoded[:data]
62
+
63
+ Sawyer::Resource.new(sawyer_agent, decoded[:data])
64
+ end
65
+
66
+ private
67
+
68
+ # Create a minimal Sawyer agent for wrapping resources
69
+ #
70
+ # The agent URL is a placeholder - we don't make any HTTP requests.
71
+ # We just need the agent to create Sawyer::Resource objects that
72
+ # provide dot-notation attribute access.
73
+ #
74
+ # @return [Sawyer::Agent]
75
+ def sawyer_agent
76
+ @sawyer_agent ||= Sawyer::Agent.new("https://example.com") do |http|
77
+ http.headers[:content_type] = "application/json"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -58,6 +58,13 @@ module Booqable
58
58
  paginate @resource, params
59
59
  end
60
60
 
61
+ # Alias for list
62
+ #
63
+ # @see list
64
+ def all(params = {})
65
+ list(params)
66
+ end
67
+
61
68
  # Find a specific resource by ID
62
69
  #
63
70
  # Retrieves a single resource by its unique identifier.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Booqable
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/booqable.rb CHANGED
@@ -25,6 +25,7 @@ require_relative "booqable/resources"
25
25
  require_relative "booqable/auth"
26
26
  require_relative "booqable/http"
27
27
  require_relative "booqable/client"
28
+ require_relative "booqable/resource_parser"
28
29
 
29
30
  # Main Booqable module providing access to the Booqable API
30
31
  #
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: booqable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hrvoje Šimić
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-10-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -138,6 +137,7 @@ files:
138
137
  - lib/booqable/middleware/raise_error.rb
139
138
  - lib/booqable/oauth_client.rb
140
139
  - lib/booqable/rate_limit.rb
140
+ - lib/booqable/resource_parser.rb
141
141
  - lib/booqable/resource_proxy.rb
142
142
  - lib/booqable/resources.json
143
143
  - lib/booqable/resources.rb
@@ -149,10 +149,9 @@ licenses:
149
149
  metadata:
150
150
  homepage_uri: https://github.com/booqable/booqable.rb
151
151
  source_code_uri: https://github.com/booqable/booqable.rb
152
- changelog_uri: https://github.com/booqable/booqable.rb/blob/master/CHANGELOG.md
152
+ changelog_uri: https://github.com/booqable/booqable.rb/blob/main/CHANGELOG.md
153
153
  documentation_uri: https://developers.booqable.com/
154
154
  bug_tracker_uri: https://github.com/booqable/booqable.rb/issues
155
- post_install_message:
156
155
  rdoc_options: []
157
156
  require_paths:
158
157
  - lib
@@ -167,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
166
  - !ruby/object:Gem::Version
168
167
  version: '0'
169
168
  requirements: []
170
- rubygems_version: 3.5.22
171
- signing_key:
169
+ rubygems_version: 3.6.9
172
170
  specification_version: 4
173
171
  summary: Official Booqable API client for Ruby.
174
172
  test_files: []