Skip to content

Commit 21d6e4f

Browse files
committed
Polish docs and harden API request handling
1 parent 726cac2 commit 21d6e4f

27 files changed

Lines changed: 457 additions & 17 deletions

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The gem is built around:
1919
- full endpoint coverage through `client.api.rest` and `client.api.upload`
2020

2121
- [Requirements](#requirements)
22+
- [Compatibility](#compatibility)
2223
- [Installation](#installation)
2324
- [Design](#design)
2425
- [Quick Start](#quick-start)
@@ -27,10 +28,12 @@ The gem is built around:
2728
- [Uploads](#uploads)
2829
- [Files](#files)
2930
- [Groups](#groups)
31+
- [Project](#project)
3032
- [Metadata](#metadata)
3133
- [Webhooks](#webhooks)
3234
- [Add-ons](#add-ons)
3335
- [Conversions](#conversions)
36+
- [Secure Delivery](#secure-delivery)
3437
- [Errors and Results](#errors-and-results)
3538
- [Request Options](#request-options)
3639
- [Raw API Access](#raw-api-access)
@@ -41,6 +44,14 @@ The gem is built around:
4144

4245
- Ruby 3.3+
4346

47+
## Compatibility
48+
49+
This gem is intended for plain Ruby applications and for framework integrations built on top of it.
50+
51+
- Use explicit `Uploadcare::Client` instances when you need multiple accounts in one process.
52+
- Use `Uploadcare.configure` and `Uploadcare.client` when one global default client is enough.
53+
- Use `client.api.rest` and `client.api.upload` when you want endpoint-level parity with the official API references.
54+
4455
## Installation
4556

4657
Add the gem to your Gemfile:
@@ -125,6 +136,30 @@ end
125136

126137
The recommended style is explicit `Uploadcare::Client` instances. Global configuration is best treated as a default.
127138

139+
## Example Usage
140+
141+
This is the shortest end-to-end flow for the main public API:
142+
143+
```ruby
144+
require "uploadcare"
145+
146+
client = Uploadcare::Client.new(
147+
public_key: ENV.fetch("UPLOADCARE_PUBLIC_KEY"),
148+
secret_key: ENV.fetch("UPLOADCARE_SECRET_KEY")
149+
)
150+
151+
file = File.open("photo.jpg", "rb") do |io|
152+
client.files.upload(io, store: true)
153+
end
154+
155+
group = client.groups.create(uuids: [file.uuid])
156+
157+
puts file.uuid
158+
puts file.cdn_url
159+
puts group.id
160+
puts group.cdn_url
161+
```
162+
128163
## Configuration
129164

130165
Use `Uploadcare.configure` to set process-wide defaults:
@@ -306,6 +341,19 @@ Common upload options:
306341
- `async: true` for URL uploads
307342
- `threads:` and `part_size:` for multipart uploads
308343

344+
If you prefer the older top-level style, the same flows can still be written through the global client:
345+
346+
```ruby
347+
Uploadcare.configure do |config|
348+
config.public_key = ENV.fetch("UPLOADCARE_PUBLIC_KEY")
349+
config.secret_key = ENV.fetch("UPLOADCARE_SECRET_KEY")
350+
end
351+
352+
file = File.open("photo.jpg", "rb") do |io|
353+
Uploadcare.files.upload(io, store: true)
354+
end
355+
```
356+
309357
## Files
310358

311359
### Find a file
@@ -329,6 +377,12 @@ files.previous_page
329377
files.all
330378
```
331379

380+
Filters and API parameters can still be passed through:
381+
382+
```ruby
383+
files = client.files.list(stored: true, removed: false, limit: 100)
384+
```
385+
332386
### Resource operations
333387

334388
```ruby
@@ -392,6 +446,18 @@ group.cdn_url
392446
group.file_cdn_urls
393447
```
394448

449+
## Project
450+
451+
Fetch the current project:
452+
453+
```ruby
454+
project = client.project.current
455+
456+
puts project.name
457+
puts project.pub_key
458+
puts project.collaborators
459+
```
460+
395461
## Metadata
396462

397463
```ruby
@@ -453,6 +519,26 @@ Document conversion `convert` returns the API response hash.
453519

454520
Video conversion `convert` returns a `Uploadcare::VideoConversion` resource.
455521

522+
## Secure Delivery
523+
524+
The gem includes signed URL generators for delivery workflows.
525+
526+
```ruby
527+
generator = Uploadcare::SignedUrlGenerators::AkamaiGenerator.new(
528+
cdn_host: "example.com",
529+
secret_key: "your_hex_encoded_akamai_secret"
530+
)
531+
532+
signed_url = generator.generate_url("a7d5645e-5cd7-4046-819f-a6a2933bafe3")
533+
```
534+
535+
Custom ACL and wildcard examples:
536+
537+
```ruby
538+
generator.generate_url("a7d5645e-5cd7-4046-819f-a6a2933bafe3", "/*")
539+
generator.generate_url("a7d5645e-5cd7-4046-819f-a6a2933bafe3", wildcard: true)
540+
```
541+
456542
## Errors and Results
457543

458544
The convenience layer raises exceptions:
@@ -525,6 +611,8 @@ client.api.upload.groups.create(files: ["uuid-1", "uuid-2"])
525611

526612
Use this layer when you want exact control over the documented endpoints or when you are wrapping the gem from another library.
527613

614+
The raw layer is part of the public surface, but it is intentionally less promoted than `client.files`, `client.groups`, and the other convenience accessors.
615+
528616
## Examples
529617

530618
- [api_examples/README.md](./api_examples/README.md): one canonical script per documented REST and Upload API endpoint

lib/uploadcare.rb

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,46 +41,48 @@ def client(config: nil, **options)
4141
Client.new(config: config || configuration, **options)
4242
end
4343

44-
# Convenience accessor for files domain.
45-
#
4644
# @return [Uploadcare::Client::FilesAccessor]
4745
def files
4846
client.files
4947
end
5048

51-
# Convenience accessor for groups domain.
52-
#
5349
# @return [Uploadcare::Client::GroupsAccessor]
5450
def groups
5551
client.groups
5652
end
5753

58-
# Convenience accessor for uploads domain.
59-
#
6054
# @return [Uploadcare::Operations::UploadRouter]
6155
def uploads
6256
client.uploads
6357
end
6458

65-
# Convenience accessor for project domain.
66-
#
6759
# @return [Uploadcare::Client::ProjectAccessor]
6860
def project
6961
client.project
7062
end
7163

64+
# Eager-load the gem namespace through Zeitwerk.
65+
#
66+
# @return [void]
7267
def eager_load!
7368
@loader.eager_load
7469
end
7570
end
7671

77-
# --- Public top-level constants (per blueprint compatibility policy) ---
72+
# Top-level aliases for the public resource classes.
7873
File = Resources::File
74+
# Alias for the group resource.
7975
Group = Resources::Group
76+
# Alias for the project resource.
8077
Project = Resources::Project
78+
# Alias for the webhook resource.
8179
Webhook = Resources::Webhook
80+
# Alias for the file metadata resource.
8281
FileMetadata = Resources::FileMetadata
82+
# Alias for the add-on execution resource.
8383
AddonExecution = Resources::AddonExecution
84+
# Alias for the document conversion resource.
8485
DocumentConversion = Resources::DocumentConversion
86+
# Alias for the video conversion resource.
8587
VideoConversion = Resources::VideoConversion
8688
end

lib/uploadcare/api/rest.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Uploadcare::Api::Rest
1919
include Uploadcare::Internal::ErrorHandler
2020
include Uploadcare::Internal::ThrottleHandler
2121

22+
# Verb name used when deciding whether params belong in the query string.
2223
HTTP_GET = 'GET'
2324

2425
# @return [Uploadcare::Configuration]
@@ -186,10 +187,11 @@ def build_request_uri(path, params, method)
186187

187188
def prepare_headers(req, method, uri, params, headers)
188189
body_content = body_content_for_signature(method, params)
189-
content_type = headers['Content-Type'] || authenticator.default_headers['Content-Type']
190+
content_type = extract_content_type(headers) || authenticator.default_headers['Content-Type']
190191
auth_headers = authenticator.headers(method, uri, body_content, content_type)
192+
normalized_headers = normalize_content_type_header(headers, content_type)
191193
req.headers.merge!(auth_headers)
192-
req.headers.merge!(headers)
194+
req.headers.merge!(normalized_headers)
193195
end
194196

195197
def body_content_for_signature(method, params)
@@ -217,6 +219,19 @@ def apply_request_options(req, request_options)
217219
req.options.open_timeout = request_options[:open_timeout] if request_options[:open_timeout]
218220
end
219221

222+
def extract_content_type(headers)
223+
headers['Content-Type'] || headers['content-type'] || headers[:content_type] || headers[:'Content-Type']
224+
end
225+
226+
def normalize_content_type_header(headers, content_type)
227+
normalized_headers = headers.dup
228+
normalized_headers.delete('content-type')
229+
normalized_headers.delete(:content_type)
230+
normalized_headers.delete(:'Content-Type')
231+
normalized_headers['Content-Type'] = content_type if content_type
232+
normalized_headers
233+
end
234+
220235
def build_uri(path, query_params = {})
221236
if query_params.empty?
222237
path

lib/uploadcare/api/rest/video_conversions.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ def initialize(rest:)
2020
# @return [Uploadcare::Result] Conversion details
2121
# @see https://uploadcare.com/api-refs/rest-api/v0.7.0/#tag/Video/operation/convertVideo
2222
def convert(paths:, options: {}, request_options: {})
23-
params = { paths: paths }.merge(options)
23+
params = { paths: paths }
24+
params[:store] = normalize_bool_param(options[:store]) if options.key?(:store)
25+
params.merge!(options.except(:store))
26+
params.compact!
2427
rest.post(path: '/convert/video/', params: params, headers: {}, request_options: request_options)
2528
end
2629

@@ -34,4 +37,16 @@ def status(token:, request_options: {})
3437
rest.get(path: "/convert/video/status/#{token}/", params: {}, headers: {},
3538
request_options: request_options)
3639
end
40+
41+
private
42+
43+
def normalize_bool_param(value)
44+
normalized = value.is_a?(String) ? value.strip.downcase : value
45+
46+
case normalized
47+
when true, 1, '1', 'true' then '1'
48+
when false, 0, '0', 'false' then '0'
49+
else value
50+
end
51+
end
3752
end

lib/uploadcare/api/upload.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,13 @@ def upload_part_to_url(presigned_url, part_data, max_retries: 3)
112112

113113
true
114114
rescue StandardError => e
115-
retries += 1
116115
if retries >= max_retries
117116
raise Uploadcare::Exception::MultipartUploadError,
118117
"Failed to upload part after #{max_retries} retries: #{e.message}"
119118
end
120119

121120
sleep(2**retries)
121+
retries += 1
122122
retry
123123
end
124124
end

lib/uploadcare/client.rb

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,51 +22,93 @@
2222
class Uploadcare::Client
2323
attr_reader :config
2424

25+
# Build a client bound to a specific configuration.
26+
#
27+
# @param config [Uploadcare::Configuration, nil] Base configuration to clone
28+
# @param options [Hash] Per-client configuration overrides
2529
def initialize(config: nil, **options)
2630
base_config = config || Uploadcare.configuration
2731
@config = base_config.with(**options)
2832
end
2933

34+
# Build a new client derived from this client.
35+
#
36+
# @param options [Hash] Configuration overrides
37+
# @return [Uploadcare::Client]
3038
def with(**options)
31-
self.class.new(config: config.with, **options)
39+
self.class.new(config: config, **options)
3240
end
3341

42+
# Access the raw endpoint-parity API.
43+
#
44+
# @return [Uploadcare::Client::Api]
3445
def api
3546
@api ||= Api.new(config: config)
3647
end
3748

49+
# Access file operations and upload helpers.
50+
#
51+
# @return [Uploadcare::Client::FilesAccessor]
3852
def files
3953
@files ||= FilesAccessor.new(client: self)
4054
end
4155

56+
# Access group operations.
57+
#
58+
# @return [Uploadcare::Client::GroupsAccessor]
4259
def groups
4360
@groups ||= GroupsAccessor.new(client: self)
4461
end
4562

63+
# Access upload routing helpers.
64+
#
65+
# @return [Uploadcare::Operations::UploadRouter]
4666
def uploads
4767
@uploads ||= Uploadcare::Operations::UploadRouter.new(client: self)
4868
end
4969

70+
# Access project operations.
71+
#
72+
# @return [Uploadcare::Client::ProjectAccessor]
5073
def project
5174
@project ||= ProjectAccessor.new(client: self)
5275
end
5376

77+
# Access webhook operations.
78+
#
79+
# @return [Uploadcare::Client::WebhooksAccessor]
5480
def webhooks
5581
@webhooks ||= WebhooksAccessor.new(client: self)
5682
end
5783

84+
# Access add-on execution helpers.
85+
#
86+
# @return [Uploadcare::Client::AddonsAccessor]
5887
def addons
5988
@addons ||= AddonsAccessor.new(client: self)
6089
end
6190

91+
# Access file metadata operations.
92+
#
93+
# @return [Uploadcare::Client::FileMetadataAccessor]
6294
def file_metadata
6395
@file_metadata ||= FileMetadataAccessor.new(client: self)
6496
end
6597

98+
# Access conversion helpers.
99+
#
100+
# @return [Uploadcare::Client::ConversionsAccessor]
66101
def conversions
67102
@conversions ||= ConversionsAccessor.new(client: self)
68103
end
69104

105+
# Upload a source through the convenience upload router.
106+
#
107+
# @param source [IO, Array<IO>, String] File object, file array, or URL
108+
# @param request_options [Hash] Per-request HTTP options
109+
# @param options [Hash] Upload options
110+
# @yield [Hash] Multipart progress callback
111+
# @return [Uploadcare::Resources::File, Array<Uploadcare::Resources::File>, Hash]
70112
def upload(source, request_options: {}, **options, &block)
71113
uploads.upload(source, request_options: request_options, **options, &block)
72114
end

0 commit comments

Comments
 (0)