rb_splash 1.0.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.
data/README.md ADDED
@@ -0,0 +1,1024 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" alt="RbSplash" width="500">
3
+ </p>
4
+
5
+ # RbSplash v1.0.0
6
+
7
+ [![license](https://img.shields.io/github/license/SandeepVattapparambil/rb_splash.svg)](https://github.com/SandeepVattapparambil/rb_splash/blob/master/LICENSE) ![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg) ![Gem Version](https://badge.fury.io/rb/rb_splash.svg) ![GitHub issues](https://img.shields.io/github/issues/SandeepVattapparambil/rb_splash.svg) ![GitHub forks](https://img.shields.io/github/forks/SandeepVattapparambil/rb_splash.svg) ![GitHub stars](https://img.shields.io/github/stars/SandeepVattapparambil/rb_splash.svg)
8
+
9
+ RbSplash is a promise-based API wrapper for the popular [Unsplash](https://unsplash.com/) platform, written in **Ruby** and ported from [wrapsplash](https://github.com/SandeepVattapparambil/wrapsplash) (TypeScript). It uses [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) for futures and [Faraday](https://github.com/lostisland/faraday) for HTTP requests.
10
+
11
+ Unsplash provides beautiful high quality free images and photos that you can download and use for any project without any attribution.
12
+
13
+ Before using the Unsplash API, you need to **register as a developer** and **read the API Guidelines.**
14
+
15
+ > **Note:** Every application must abide by the [API Guidelines](https://unsplash.com/documentation). Specifically, remember to hotlink images and trigger a download when appropriate.
16
+
17
+ ## Table of Contents
18
+ <!--ts-->
19
+ * [About](#rbSplash-v100)
20
+ * [Installation](#installation)
21
+ * [Sample Usage](#sample-usage)
22
+ * [Development](#development)
23
+ * [New API Aliases](#new-api-aliases)
24
+ * [Dependencies](#dependencies)
25
+ * [API Documentation](#api-documentation)
26
+ * [Schema](#schema)
27
+ * [Location](#location)
28
+ * [Summary Objects](#summary-objects)
29
+ * [Error Messages](#error-messages)
30
+ * [Authorization](#authorization)
31
+ * [Public Actions](#public-actions)
32
+ * [User Authentication](#user-authentication)
33
+ * [RbSplash init()](#rbsplash-init)
34
+ * [Generate Bearer Token](#generate-bearer-token)
35
+ * [Users APIs](#users-apis)
36
+ * [Get User's Public Profile](#get-users-public-profile)
37
+ * [Get User's Portfolio Link](#get-users-portfolio-link)
38
+ * [Get User's Photos](#get-users-photos)
39
+ * [Get User Liked Photos](#get-user-liked-photos)
40
+ * [Get User's Collections](#get-users-collections)
41
+ * [Get User's Statistics](#get-users-statistics)
42
+ * [Photos APIs](#photos-apis)
43
+ * [List Photos](#list-photos)
44
+ * [List Curated Photos](#list-curated-photos)
45
+ * [Get a Photo by Id](#get-a-photo-by-id)
46
+ * [Get a Random Photo](#get-a-random-photo)
47
+ * [Get a Photo's Statistics](#get-a-photos-statistics)
48
+ * [Get a Photo's Download Link](#get-a-photos-download-link)
49
+ * [Update a Photo](#update-a-photo)
50
+ * [Like a Photo](#like-a-photo)
51
+ * [Unlike a Photo](#unlike-a-photo)
52
+ * [Search APIs](#search-apis)
53
+ * [Search Photos](#search-photos)
54
+ * [Search Collections](#search-collections)
55
+ * [Search Users](#search-users)
56
+ * [Current User APIs](#current-user-apis)
57
+ * [Get the User's Profile](#get-users-profile)
58
+ * [Update User's Profile](#update-users-profile)
59
+ * [Stats APIs](#stats-apis)
60
+ * [Stats Totals](#stats-totals)
61
+ * [Stats Month](#stats-month)
62
+ * [Collections APIs](#collections-apis)
63
+ * [Link Relations](#link-relations)
64
+ * [List Collections](#list-collections)
65
+ * [List Featured Collections](#list-featured-collections)
66
+ * [List Curated Collections](#list-curated-collections)
67
+ * [Get a Collection](#get-a-collection)
68
+ * [Get a Curated Collection](#get-a-curated-collection)
69
+ * [Get a Collection's Photos](#get-a-collections-photos)
70
+ * [Get a Curated Collection's Photos](#get-a-curated-collections-photos)
71
+ * [List a Collection's Related Collections](#list-a-collections-related-collections)
72
+ * [Create a New Collection](#create-a-new-collection)
73
+ * [Update an Existing Collection](#update-an-existing-collection)
74
+ * [Delete a Collection](#delete-a-collection)
75
+ * [Add a Photo to a Collection](#add-a-photo-to-a-collection)
76
+ * [Remove a Photo from a Collection](#remove-a-photo-from-a-collection)
77
+ * [Tests](#tests)
78
+ * [License](#license)
79
+ * [Acknowledgements](#acknowledgements)
80
+ <!--te-->
81
+
82
+ ## Installation
83
+
84
+ Add this line to your application's Gemfile:
85
+
86
+ ```ruby
87
+ gem "rb_splash"
88
+ ```
89
+
90
+ Then execute:
91
+
92
+ ```sh
93
+ bundle install
94
+ ```
95
+
96
+ Or install it directly:
97
+
98
+ ```sh
99
+ gem install rb_splash
100
+ ```
101
+
102
+ ## Sample Usage
103
+
104
+ ```ruby
105
+ require "rb_splash"
106
+
107
+ api = RbSplash::WrapSplashApi.new
108
+
109
+ # Initialize with a bearer token
110
+ api.init(bearer_token: ENV["UNSPLASH_BEARER_TOKEN"])
111
+
112
+ # Or initialize with access key credentials
113
+ api.init(
114
+ access_key: ENV["UNSPLASH_ACCESS_KEY"],
115
+ secret_key: ENV["UNSPLASH_SECRET_KEY"],
116
+ redirect_uri: ENV["UNSPLASH_REDIRECT_URI"],
117
+ code: ENV["UNSPLASH_AUTH_CODE"]
118
+ )
119
+
120
+ # Get the current user profile
121
+ profile = api.get_current_user_profile.value!
122
+
123
+ # Search for photos
124
+ results = api.search("nature", per_page: 5).value!
125
+
126
+ # Get a random photo
127
+ photo = api.get_random_photo(orientation: "landscape").value!
128
+ ```
129
+
130
+ All API methods return a `Concurrent::Promises::Future`. Call `.value!` to get the result or raise on error.
131
+
132
+ ## Development
133
+
134
+ ```sh
135
+ git clone https://github.com/SandeepVattapparambil/rb_splash.git
136
+ cd rb_splash
137
+ bundle install
138
+ bundle exec rspec
139
+ ```
140
+
141
+ ## New API Aliases
142
+
143
+ The library includes more descriptive convenience methods such as `get_photo`, `get_random_photo`, `create_collection`, and `update_collection`. The original method names remain available for backward compatibility.
144
+
145
+ ```ruby
146
+ # Original names
147
+ api.get_a_photo("photo-id")
148
+ api.get_a_random_photo(orientation: "landscape")
149
+ api.create_new_collection("My Collection")
150
+ api.update_existing_collection("cid", "Title")
151
+
152
+ # Convenience aliases
153
+ api.get_photo("photo-id")
154
+ api.get_random_photo(orientation: "landscape")
155
+ api.create_collection("My Collection")
156
+ api.update_collection("cid", "Title")
157
+ ```
158
+
159
+ ## Dependencies
160
+
161
+ This library depends on:
162
+ - [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) - for promise-based async futures
163
+ - [faraday](https://github.com/lostisland/faraday) - for HTTP requests
164
+ - [faraday-net_http](https://github.com/lostisland/faraday-net_http) - Net::HTTP adapter for Faraday
165
+
166
+ ## API Documentation
167
+
168
+ ### Schema
169
+
170
+ #### Location
171
+ The API base URL is `https://api.unsplash.com/`. Responses are sent as JSON.
172
+
173
+ #### Summary Objects
174
+ When retrieving a list of objects, an abbreviated or summary version of that object is returned - i.e., a subset of its attributes. To get a full detailed version of that object, fetch it individually.
175
+
176
+ #### Error Messages
177
+ If an error occurs, whether on the server or client side, the error message(s) will be returned in an `errors` array.
178
+
179
+ For example:
180
+ ```
181
+ 422 Unprocessable Entity
182
+ ```
183
+ ```json
184
+ {
185
+ "errors": ["Username is missing", "Password cannot be blank"]
186
+ }
187
+ ```
188
+
189
+ ### Authorization
190
+
191
+ #### Public Actions
192
+ Many actions can be performed without requiring authentication from a specific user. For example, downloading a photo does not require a user to log in.
193
+
194
+ To authenticate requests in this way, pass your application's access key via the HTTP `Authorization` header:
195
+ ```
196
+ Authorization: Client-ID YOUR_ACCESS_KEY
197
+ ```
198
+
199
+ If only your access key is sent, attempting to perform non-public actions that require user authorization will result in a `401 Unauthorized response`.
200
+
201
+ #### User Authentication
202
+ The Unsplash API uses OAuth2 to authenticate and authorize Unsplash users. Unsplash's OAuth2 paths live at `https://unsplash.com/oauth/`.
203
+
204
+ Before using RbSplash:
205
+ - Developers are required to create a developer account from [Unsplash](https://unsplash.com/developers).
206
+ - Create a new App from Your Apps page.
207
+ - Get the `Access Key`, `Secret Key`, `Callback URLs`, and `Authorization code`.
208
+ - If you have a Bearer Token, then its super, or else you can generate it using **RbSplash**.
209
+
210
+ > **Note:** `Authorization code` can be obtained by clicking the `Authorize` link next to `Callback URLs`. Also `Authorization code` is a one-time use code, you have to generate it again if the action fails!
211
+
212
+ #### RbSplash init()
213
+ The RbSplash instance has to be initialized with your credentials obtained from the Unsplash developer account for programmatic access. These credentials are passed to the `init()` function as options. The following example shows all the available options.
214
+
215
+ ```ruby
216
+ api.init(
217
+ access_key: ENV['UNSPLASH_ACCESS_KEY'],
218
+ secret_key: ENV['UNSPLASH_SECRET_KEY'],
219
+ redirect_uri: ENV['UNSPLASH_REDIRECT_URI'],
220
+ code: ENV['UNSPLASH_AUTH_CODE'],
221
+ bearer_token: ENV['UNSPLASH_BEARER_TOKEN']
222
+ )
223
+ ```
224
+
225
+ If you have a `bearer_token`, then only the bearer token has to be passed in.
226
+
227
+ ```ruby
228
+ api.init(bearer_token: ENV['UNSPLASH_BEARER_TOKEN'])
229
+ ```
230
+
231
+ #### Generate Bearer Token
232
+ A method to generate a Bearer Token for `write_access` to private data. The `init()` method in this case requires `access_key`, `secret_key`, `redirect_uri`, and `code`.
233
+
234
+ > **Note:** No parameters are required for this method.
235
+
236
+ ```ruby
237
+ api = RbSplash::WrapSplashApi.new
238
+
239
+ api.init(
240
+ access_key: ENV['UNSPLASH_ACCESS_KEY'],
241
+ secret_key: ENV['UNSPLASH_SECRET_KEY'],
242
+ redirect_uri: ENV['UNSPLASH_REDIRECT_URI'],
243
+ code: ENV['UNSPLASH_AUTH_CODE']
244
+ )
245
+
246
+ result = api.generate_bearer_token.value!
247
+ puts result
248
+ ```
249
+
250
+ If successful, the response body will be a JSON representation of your user's access token a.k.a bearer token:
251
+
252
+ ```json
253
+ {
254
+ "access_token": "091343ce13c8ae780065ecb3b13dc903475dd22cb78a05503c2e0c69c5e98044",
255
+ "token_type": "bearer",
256
+ "scope": "public read_photos write_photos",
257
+ "created_at": 1436544465
258
+ }
259
+ ```
260
+
261
+ And once you have your `bearer_token` you can use it in your app like this:
262
+
263
+ ```ruby
264
+ api.init(bearer_token: ENV['UNSPLASH_BEARER_TOKEN'])
265
+ ```
266
+
267
+ ### Users APIs
268
+
269
+ #### Get User's Public Profile
270
+ A promise factory to retrieve public details on a given user.
271
+ ```
272
+ GET /users/:username
273
+ ```
274
+
275
+ ##### Parameters
276
+
277
+ | Parameter | Type | Description | Optional | Default |
278
+ | --------- | ---- | ----------- | -------- | ------- |
279
+ | **username** | *string* | The username of the particular user | no | |
280
+ | **width** | *number* | Width of the profile picture in pixels | yes | |
281
+ | **height** | *number* | Height of the profile picture in pixels | yes | |
282
+
283
+ > **Note:** When optional **height** & **width** are specified the profile image will be included in the "profile_image" object as "custom".
284
+
285
+ ```ruby
286
+ api.get_public_profile('<username>', width: 600, height: 600)
287
+ ```
288
+
289
+ #### Get User's Portfolio Link
290
+ A promise factory to retrieve a single user's portfolio link.
291
+ ```
292
+ GET /users/:username/portfolio
293
+ ```
294
+
295
+ ##### Parameters
296
+
297
+ | Parameter | Type | Description | Optional | Default |
298
+ | --------- | ---- | ----------- | -------- | ------- |
299
+ | **username** | *string* | The username of the particular user | no | |
300
+
301
+ ```ruby
302
+ api.get_user_portfolio('<username>')
303
+ ```
304
+
305
+ #### Get User's Photos
306
+ A promise factory to get a list of photos uploaded by a particular user.
307
+ ```
308
+ GET /users/:username/photos
309
+ ```
310
+
311
+ ##### Parameters
312
+
313
+ | Parameter | Type | Description | Optional | Default |
314
+ | --------- | ---- | ----------- | -------- | ------- |
315
+ | **username** | *string* | The username of the particular user | no | |
316
+ | **page** | *number* | Page number to retrieve | yes | 1 |
317
+ | **per_page** | *number* | Number of items per page | yes | 10 |
318
+ | **stats** | *boolean* | Show the stats for each user's photo | yes | false |
319
+ | **resolution** | *string* | The frequency of the stats | yes | days |
320
+ | **quantity** | *number* | The amount for each stat | yes | 30 |
321
+ | **order_by** | *string* | How to sort the photos. (`Valid values: latest, oldest, popular`) | yes | latest |
322
+
323
+ ```ruby
324
+ api.get_user_photos('<username>', page: 1, per_page: 10, order_by: 'latest')
325
+ ```
326
+
327
+ #### Get User Liked Photos
328
+ A promise factory to get a list of photos liked by a user.
329
+ ```
330
+ GET /users/:username/likes
331
+ ```
332
+
333
+ ##### Parameters
334
+
335
+ | Parameter | Type | Description | Optional | Default |
336
+ | --------- | ---- | ----------- | -------- | ------- |
337
+ | **username** | *string* | The username of the particular user | no | |
338
+ | **page** | *number* | Page number to retrieve | yes | 1 |
339
+ | **per_page** | *number* | Number of items per page | yes | 10 |
340
+ | **order_by** | *string* | How to sort the photos. (`Valid values: latest, oldest, popular`) | yes | latest |
341
+
342
+ ```ruby
343
+ api.get_user_liked_photos('<username>', page: 1, per_page: 10, order_by: 'latest')
344
+ ```
345
+
346
+ #### Get User's Collections
347
+ A promise factory to get a list of collections created by the user.
348
+ ```
349
+ GET /users/:username/collections
350
+ ```
351
+
352
+ ##### Parameters
353
+
354
+ | Parameter | Type | Description | Optional | Default |
355
+ | --------- | ---- | ----------- | -------- | ------- |
356
+ | **username** | *string* | The username of the particular user | no | |
357
+ | **page** | *number* | Page number to retrieve | yes | 1 |
358
+ | **per_page** | *number* | Number of items per page | yes | 10 |
359
+
360
+ ```ruby
361
+ api.get_user_collections('<username>', page: 1, per_page: 10)
362
+ ```
363
+
364
+ #### Get User's Statistics
365
+ A promise factory to get a user's account statistics.
366
+ ```
367
+ GET /users/:username/statistics
368
+ ```
369
+
370
+ ##### Parameters
371
+
372
+ | Parameter | Type | Description | Optional | Default |
373
+ | --------- | ---- | ----------- | -------- | ------- |
374
+ | **username** | *string* | The username of the particular user | no | |
375
+ | **resolution** | *string* | The frequency of the stats | yes | days |
376
+ | **quantity** | *number* | The amount for each stat | yes | 30 |
377
+
378
+ ```ruby
379
+ api.get_user_statistics('<username>', resolution: 'days', quantity: 30)
380
+ ```
381
+
382
+ ### Photos APIs
383
+
384
+ #### List Photos
385
+ A promise factory to get a single page from the list of all photos.
386
+ ```
387
+ GET /photos
388
+ ```
389
+
390
+ ##### Parameters
391
+
392
+ | Parameter | Type | Description | Optional | Default |
393
+ | --------- | ---- | ----------- | -------- | ------- |
394
+ | **page** | *number* | Page number to retrieve | yes | 1 |
395
+ | **per_page** | *number* | Number of items per page | yes | 10 |
396
+ | **order_by** | *string* | How to sort the photos. (`Valid values: latest, oldest, popular`) | yes | latest |
397
+
398
+ ```ruby
399
+ api.list_photos(page: 1, per_page: 10, order_by: 'latest')
400
+ ```
401
+
402
+ #### List Curated Photos
403
+ A promise factory to get a single page from the list of the curated photos.
404
+ ```
405
+ GET /photos/curated
406
+ ```
407
+
408
+ ##### Parameters
409
+
410
+ | Parameter | Type | Description | Optional | Default |
411
+ | --------- | ---- | ----------- | -------- | ------- |
412
+ | **page** | *number* | Page number to retrieve | yes | 1 |
413
+ | **per_page** | *number* | Number of items per page | yes | 10 |
414
+ | **order_by** | *string* | How to sort the photos. (`Valid values: latest, oldest, popular`) | yes | latest |
415
+
416
+ ```ruby
417
+ api.list_curated_photos(page: 1, per_page: 10, order_by: 'latest')
418
+ ```
419
+
420
+ #### Get a Photo by Id
421
+ A promise factory to retrieve a single photo.
422
+ ```
423
+ GET /photos/:id
424
+ ```
425
+
426
+ ##### Parameters
427
+
428
+ | Parameter | Type | Description | Optional | Default |
429
+ | --------- | ---- | ----------- | -------- | ------- |
430
+ | **id** | *string* | The photo's ID | no | |
431
+ | **width** | *number* | Image width in pixels | yes | |
432
+ | **height** | *number* | Image height in pixels | yes | |
433
+ | **rect** | *string* | 4 comma-separated integers representing x, y, width, height of the cropped rectangle | yes | |
434
+
435
+ > **Note:** Supplying the optional **width** or **height** parameters will result in the custom photo URL being added to the urls object.
436
+
437
+ ```ruby
438
+ api.get_a_photo('<photo-id>', width: 500, height: 500, rect: '0,0,500,500')
439
+ # or use the alias
440
+ api.get_photo('<photo-id>', width: 500, height: 500)
441
+ ```
442
+
443
+ #### Get a Random Photo
444
+ A promise factory to retrieve a single random photo, given optional filters.
445
+ ```
446
+ GET /photos/random
447
+ ```
448
+
449
+ ##### Parameters
450
+
451
+ > **Note:** All parameters are optional, and can be combined to narrow the pool of photos from which a random one will be chosen.
452
+
453
+ | Parameter | Type | Description | Optional | Default |
454
+ | --------- | ---- | ----------- | -------- | ------- |
455
+ | **collections** | *string* | The public collection ID('s) to filter selection. If multiple, comma-separated | yes | |
456
+ | **featured** | *boolean* | Limit selection to featured photos | yes | false |
457
+ | **username** | *string* | Limit selection to a single user | yes | |
458
+ | **query** | *string* | Limit selection to photos matching a search term | yes | |
459
+ | **width** | *number* | The image width in pixels | yes | |
460
+ | **height** | *number* | The image height in pixels | yes | |
461
+ | **orientation** | *string* | Filter search results by photo orientation. (`Valid values: landscape, portrait, squarish`) | yes | landscape |
462
+ | **count** | *number* | The number of photos to return. (`max: 30`) | yes | 1 |
463
+
464
+ > **Note:** You can't use the collections and query parameters in the same request.
465
+ > When supplying a **count** parameter - and only then - the response will be an array of photos, even if the value of **count** is 1.
466
+
467
+ ```ruby
468
+ api.get_a_random_photo(orientation: 'landscape', count: 5)
469
+ # or use the alias
470
+ api.get_random_photo(orientation: 'portrait')
471
+ ```
472
+
473
+ #### Get a Photo's Statistics
474
+ A promise factory to retrieve total number of downloads, views and likes of a single photo, as well as the historical breakdown of these stats in a specific timeframe (default is 30 days).
475
+ ```
476
+ GET /photos/:id/statistics
477
+ ```
478
+
479
+ ##### Parameters
480
+
481
+ | Parameter | Type | Description | Optional | Default |
482
+ | --------- | ---- | ----------- | -------- | ------- |
483
+ | **id** | *string* | The photo's ID | no | |
484
+ | **resolution** | *string* | The frequency of the stats | yes | days |
485
+ | **quantity** | *number* | The amount for each stat | yes | 30 |
486
+
487
+ > **Note:** Currently, the only resolution param supported is "days". The quantity param can be any number between 1 and 30.
488
+
489
+ ```ruby
490
+ api.get_photo_statistics('<photo-id>', resolution: 'days', quantity: 10)
491
+ ```
492
+
493
+ #### Get a Photo's Download Link
494
+ A promise factory to retrieve a single photo's download link. Preferably hit this endpoint if a photo is downloaded in your application for use (example: to be displayed on a blog article, to be shared on social media, to be remixed, etc).
495
+ ```
496
+ GET /photos/:id/download
497
+ ```
498
+
499
+ ##### Parameters
500
+
501
+ | Parameter | Type | Description | Optional | Default |
502
+ | --------- | ---- | ----------- | -------- | ------- |
503
+ | **id** | *string* | The photo's ID | no | |
504
+
505
+ > **Note:** This is different than the concept of a view, which is tracked automatically when you hotlink an image.
506
+
507
+ ```ruby
508
+ api.get_photo_link('<photo-id>')
509
+ ```
510
+
511
+ #### Update a Photo
512
+ A promise factory to update a photo on behalf of the logged-in user. This requires the `write_photos` scope and `bearer_token`.
513
+ ```
514
+ PUT /photos/:id
515
+ ```
516
+
517
+ ##### Parameters
518
+
519
+ | Parameter | Type | Description | Optional | Default |
520
+ | --------- | ---- | ----------- | -------- | ------- |
521
+ | **id** | *string* | The photo's ID | no | |
522
+ | **location** | *hash* | The location hash holding location data | yes | |
523
+ | **exif** | *hash* | The exif hash holding exif data | yes | |
524
+
525
+ > **Note:** **Exchangeable image file format** (officially Exif, according to JEIDA/JEITA/CIPA specifications) is a standard that specifies the formats for images, sound, and ancillary tags used by digital cameras (including smartphones), scanners and other systems handling image and sound files recorded by digital cameras. [Read more](https://en.wikipedia.org/wiki/Exif)
526
+
527
+ ##### location & exif hashes
528
+
529
+ | Hash Key | Description |
530
+ | --------- | ----------- |
531
+ | location[:latitude] | The photo location's latitude (Optional) |
532
+ | location[:longitude] | The photo location's longitude (Optional) |
533
+ | location[:name] | The photo location's name (Optional) |
534
+ | location[:city] | The photo location's city (Optional) |
535
+ | location[:country] | The photo location's country (Optional) |
536
+ | location[:confidential] | The photo location's confidentiality (Optional) |
537
+ | exif[:make] | Camera's brand (Optional) |
538
+ | exif[:model] | Camera's model (Optional) |
539
+ | exif[:exposure_time] | Camera's exposure time (Optional) |
540
+ | exif[:aperture_value] | Camera's aperture value (Optional) |
541
+ | exif[:focal_length] | Camera's focal length (Optional) |
542
+ | exif[:iso_speed_ratings] | Camera's iso (Optional) |
543
+
544
+ ```ruby
545
+ api.update_photo('<photo-id>', location: { country: 'INDIA' }, exif: { make: 'Canon' })
546
+ ```
547
+
548
+ #### Like a Photo
549
+ A promise factory to like a photo on behalf of the logged-in user. This requires the `write_likes` scope.
550
+ ```
551
+ POST /photos/:id/like
552
+ ```
553
+
554
+ ##### Parameters
555
+
556
+ | Parameter | Type | Description | Optional | Default |
557
+ | --------- | ---- | ----------- | -------- | ------- |
558
+ | **id** | *string* | The photo's ID | no | |
559
+
560
+ > **Note:** This action is idempotent; sending the POST request to a single photo multiple times has no additional effect.
561
+
562
+ ```ruby
563
+ api.like_photo('<photo-id>')
564
+ ```
565
+
566
+ #### Unlike a Photo
567
+ A promise factory to remove a user's like of a photo.
568
+ ```
569
+ DELETE /photos/:id/like
570
+ ```
571
+
572
+ ##### Parameters
573
+
574
+ | Parameter | Type | Description | Optional | Default |
575
+ | --------- | ---- | ----------- | -------- | ------- |
576
+ | **id** | *string* | The photo's ID | no | |
577
+
578
+ > **Note:** This action is idempotent; sending the DELETE request to a single photo multiple times has no additional effect.
579
+
580
+ ```ruby
581
+ api.unlike_photo('<photo-id>')
582
+ ```
583
+
584
+ ### Search APIs
585
+
586
+ #### Search Photos
587
+ A promise factory to get a single page of photo results for a particular query.
588
+ ```
589
+ GET /search/photos
590
+ ```
591
+
592
+ ##### Parameters
593
+
594
+ | Parameter | Type | Description | Optional | Default |
595
+ | --------- | ---- | ----------- | -------- | ------- |
596
+ | **query** | *string* | The search query | no | |
597
+ | **page** | *number* | Page number to retrieve | yes | 1 |
598
+ | **per_page** | *number* | Number of items per page | yes | 10 |
599
+ | **collections** | *string* | Collection ID('s) to narrow search. If multiple, comma-separated. | yes | |
600
+ | **orientation** | *string* | Filter search results by photo orientation. (`Valid values: landscape, portrait, squarish`) | yes | |
601
+
602
+ ```ruby
603
+ api.search('cars', page: 1, per_page: 10, orientation: 'landscape')
604
+ ```
605
+
606
+ #### Search Collections
607
+ A promise factory to get a single page of collection results for a query.
608
+ ```
609
+ GET /search/collections
610
+ ```
611
+
612
+ ##### Parameters
613
+
614
+ | Parameter | Type | Description | Optional | Default |
615
+ | --------- | ---- | ----------- | -------- | ------- |
616
+ | **query** | *string* | The search query | no | |
617
+ | **page** | *number* | Page number to retrieve | yes | 1 |
618
+ | **per_page** | *number* | Number of items per page | yes | 10 |
619
+
620
+ ```ruby
621
+ api.search_collections('cars', page: 1, per_page: 10)
622
+ ```
623
+
624
+ #### Search Users
625
+ A promise factory to get a single page of user results for a query.
626
+ ```
627
+ GET /search/users
628
+ ```
629
+
630
+ ##### Parameters
631
+
632
+ | Parameter | Type | Description | Optional | Default |
633
+ | --------- | ---- | ----------- | -------- | ------- |
634
+ | **query** | *string* | The search query | no | |
635
+ | **page** | *number* | Page number to retrieve | yes | 1 |
636
+ | **per_page** | *number* | Number of items per page | yes | 10 |
637
+
638
+ ```ruby
639
+ api.search_users('<search-keyword>', page: 1, per_page: 10)
640
+ ```
641
+
642
+ ### Current User APIs
643
+
644
+ #### Get User's Profile
645
+ A promise factory to get the current user's profile. To access a user's private data, the user is required to authorize the `read_user` scope. Without it, this request will return a `403 Forbidden response`.
646
+ ```
647
+ GET /me
648
+ ```
649
+
650
+ > **Note:** No parameters are required.
651
+
652
+ > **Note:** Without a Bearer token (i.e. using a `Client-ID token`) this request will return a `401 Unauthorized` response.
653
+
654
+ ```ruby
655
+ api.get_current_user_profile
656
+ ```
657
+
658
+ #### Update User's Profile
659
+ A promise factory to update the current user's profile.
660
+ ```
661
+ PUT /me
662
+ ```
663
+
664
+ ##### Parameters
665
+
666
+ | Parameter | Type | Description | Optional | Default |
667
+ | --------- | ---- | ----------- | -------- | ------- |
668
+ | **username** | *string* | The username of the current user | yes | |
669
+ | **first_name** | *string* | The first name of the current user | yes | |
670
+ | **last_name** | *string* | The last name of the current user | yes | |
671
+ | **email** | *string* | The email of the current user | yes | |
672
+ | **url** | *string* | The portfolio/personal URL of the current user | yes | |
673
+ | **location** | *string* | The location of the current user | yes | |
674
+ | **bio** | *string* | The about/bio of the current user | yes | |
675
+ | **instagram_username** | *string* | The Instagram username of the current user | yes | |
676
+
677
+ > **Note:** This action requires the `write_user` scope. Without it, it will return a `403 Forbidden response`.
678
+
679
+ ```ruby
680
+ api.update_current_user_profile(username: 'mock-user', first_name: 'Mock', bio: 'Testing')
681
+ ```
682
+
683
+ ### Stats APIs
684
+
685
+ #### Stats Totals
686
+ A promise factory to get a list of counts for all of Unsplash.
687
+ ```
688
+ GET /stats/total
689
+ ```
690
+
691
+ ```ruby
692
+ api.get_stats_totals
693
+ ```
694
+
695
+ ##### Response
696
+ ```
697
+ 200 OK
698
+ ```
699
+ ```json
700
+ {
701
+ "total_stats": {
702
+ "photos": 10000,
703
+ "downloads": 2000,
704
+ "views": 5000,
705
+ "likes": 800,
706
+ "photographers": 100,
707
+ "pixels": 200000,
708
+ "downloads_per_second": 10,
709
+ "views_per_second": 20,
710
+ "developers": 20,
711
+ "applications": 50,
712
+ "requests": 8000
713
+ }
714
+ }
715
+ ```
716
+
717
+ #### Stats Month
718
+ A promise factory to get the overall Unsplash stats for the past 30 days.
719
+ ```
720
+ GET /stats/month
721
+ ```
722
+
723
+ ```ruby
724
+ api.get_stats_month
725
+ ```
726
+
727
+ ##### Response
728
+ ```
729
+ 200 OK
730
+ ```
731
+ ```json
732
+ {
733
+ "month_stats": {
734
+ "downloads": 20,
735
+ "views": 200,
736
+ "likes": 60,
737
+ "new_photos": 10,
738
+ "new_photographers": 5,
739
+ "new_pixels": 2000,
740
+ "new_developers": 8,
741
+ "new_applications": 5,
742
+ "new_requests": 100
743
+ }
744
+ }
745
+ ```
746
+
747
+ ### Collections APIs
748
+
749
+ #### Link Relations
750
+ Collections have the following link relations:
751
+
752
+ | rel | Description |
753
+ | --- | ----------- |
754
+ | `self` | API location of this collection |
755
+ | `html` | HTML location of this collection |
756
+ | `photos` | API location of this collection's photos |
757
+ | `related` | API location of this collection's related collections (Non-curated collections only) |
758
+ | `download` | Download location of this collection's zip file (Curated collections only) |
759
+
760
+ #### List Collections
761
+ A promise factory to get a single page from the list of all collections.
762
+ ```
763
+ GET /collections
764
+ ```
765
+
766
+ ##### Parameters
767
+
768
+ | Parameter | Type | Description | Optional | Default |
769
+ | --------- | ---- | ----------- | -------- | ------- |
770
+ | **page** | *number* | Page number to retrieve | yes | 1 |
771
+ | **per_page** | *number* | Number of items per page | yes | 10 |
772
+
773
+ ```ruby
774
+ api.list_collections(page: 1, per_page: 10)
775
+ ```
776
+
777
+ #### List Featured Collections
778
+ A promise factory to get a single page from the list of featured collections.
779
+ ```
780
+ GET /collections/featured
781
+ ```
782
+
783
+ ##### Parameters
784
+
785
+ | Parameter | Type | Description | Optional | Default |
786
+ | --------- | ---- | ----------- | -------- | ------- |
787
+ | **page** | *number* | Page number to retrieve | yes | 1 |
788
+ | **per_page** | *number* | Number of items per page | yes | 10 |
789
+
790
+ ```ruby
791
+ api.list_featured_collections(page: 1, per_page: 10)
792
+ ```
793
+
794
+ #### List Curated Collections
795
+ A promise factory to get a single page from the list of curated collections.
796
+ ```
797
+ GET /collections/curated
798
+ ```
799
+
800
+ ##### Parameters
801
+
802
+ | Parameter | Type | Description | Optional | Default |
803
+ | --------- | ---- | ----------- | -------- | ------- |
804
+ | **page** | *number* | Page number to retrieve | yes | 1 |
805
+ | **per_page** | *number* | Number of items per page | yes | 10 |
806
+
807
+ ```ruby
808
+ api.list_curated_collections(page: 1, per_page: 10)
809
+ ```
810
+
811
+ #### Get a Collection
812
+ A promise factory to retrieve a single collection. To view a user's private collections, the `read_collections` scope is required.
813
+ ```
814
+ GET /collections/:id
815
+ ```
816
+
817
+ ##### Parameters
818
+
819
+ | Parameter | Type | Description | Optional | Default |
820
+ | --------- | ---- | ----------- | -------- | ------- |
821
+ | **id** | *string* | The collection ID | no | |
822
+
823
+ ```ruby
824
+ api.get_collection('<collection-id>')
825
+ ```
826
+
827
+ #### Get a Curated Collection
828
+ A promise factory to retrieve a single curated collection. To view a user's private collections, the `read_collections` scope is required.
829
+ ```
830
+ GET /collections/curated/:id
831
+ ```
832
+
833
+ ##### Parameters
834
+
835
+ | Parameter | Type | Description | Optional | Default |
836
+ | --------- | ---- | ----------- | -------- | ------- |
837
+ | **id** | *string* | The collection ID | no | |
838
+
839
+ ```ruby
840
+ api.get_curated_collection('<curated-collection-id>')
841
+ ```
842
+
843
+ #### Get a Collection's Photos
844
+ A promise factory to retrieve a collection's photos.
845
+ ```
846
+ GET /collections/:id/photos
847
+ ```
848
+
849
+ ##### Parameters
850
+
851
+ | Parameter | Type | Description | Optional | Default |
852
+ | --------- | ---- | ----------- | -------- | ------- |
853
+ | **id** | *string* | The collection ID | no | |
854
+ | **page** | *number* | Page number to retrieve | yes | 1 |
855
+ | **per_page** | *number* | Number of items per page | yes | 10 |
856
+
857
+ ```ruby
858
+ api.get_collection_photos('<collection-id>', page: 1, per_page: 10)
859
+ ```
860
+
861
+ #### Get a Curated Collection's Photos
862
+ A promise factory to retrieve a curated collection's photos.
863
+ ```
864
+ GET /collections/curated/:id/photos
865
+ ```
866
+
867
+ ##### Parameters
868
+
869
+ | Parameter | Type | Description | Optional | Default |
870
+ | --------- | ---- | ----------- | -------- | ------- |
871
+ | **id** | *string* | The collection ID | no | |
872
+ | **page** | *number* | Page number to retrieve | yes | 1 |
873
+ | **per_page** | *number* | Number of items per page | yes | 10 |
874
+
875
+ ```ruby
876
+ api.get_curated_collection_photos('<curated-collection-id>', page: 1, per_page: 10)
877
+ ```
878
+
879
+ #### List a Collection's Related Collections
880
+ A promise factory to retrieve a list of collections related to this one.
881
+ ```
882
+ GET /collections/:id/related
883
+ ```
884
+
885
+ ##### Parameters
886
+
887
+ | Parameter | Type | Description | Optional | Default |
888
+ | --------- | ---- | ----------- | -------- | ------- |
889
+ | **id** | *string* | The collection ID | no | |
890
+
891
+ ```ruby
892
+ api.list_related_collections('<collection-id>')
893
+ ```
894
+
895
+ #### Create a New Collection
896
+ A promise factory to create a new collection. This requires the `write_collections` scope.
897
+ ```
898
+ POST /collections
899
+ ```
900
+
901
+ ##### Parameters
902
+
903
+ | Parameter | Type | Description | Optional | Default |
904
+ | --------- | ---- | ----------- | -------- | ------- |
905
+ | **title** | *string* | The title of the collection | no | |
906
+ | **description** | *string* | The collection's description | yes | |
907
+ | **private_collection** | *boolean* | Whether to make this collection private | yes | false |
908
+
909
+ ```ruby
910
+ api.create_new_collection('My Collection', description: 'desc', private_collection: true)
911
+ # or use the alias
912
+ api.create_collection('My Collection')
913
+ ```
914
+
915
+ #### Update an Existing Collection
916
+ A promise factory to update an existing collection belonging to the logged-in user. This requires the `write_collections` scope.
917
+ ```
918
+ PUT /collections/:id
919
+ ```
920
+
921
+ ##### Parameters
922
+
923
+ | Parameter | Type | Description | Optional | Default |
924
+ | --------- | ---- | ----------- | -------- | ------- |
925
+ | **id** | *string* | The collection ID | no | |
926
+ | **title** | *string* | The title of the collection | yes | |
927
+ | **description** | *string* | The collection's description | yes | |
928
+ | **private_collection** | *boolean* | Whether to make this collection private | yes | false |
929
+
930
+ ```ruby
931
+ api.update_existing_collection('<collection-id>', 'New Title', description: 'Updated')
932
+ # or use the alias
933
+ api.update_collection('<collection-id>', 'New Title')
934
+ ```
935
+
936
+ #### Delete a Collection
937
+ A promise factory to delete a collection belonging to the logged-in user. This requires the `write_collections` scope.
938
+ ```
939
+ DELETE /collections/:id
940
+ ```
941
+
942
+ ##### Parameters
943
+
944
+ | Parameter | Type | Description | Optional | Default |
945
+ | --------- | ---- | ----------- | -------- | ------- |
946
+ | **id** | *string* | The collection ID | no | |
947
+
948
+ ```ruby
949
+ api.delete_collection('<collection-id>')
950
+ ```
951
+
952
+ #### Add a Photo to a Collection
953
+ A promise factory to add a photo to one of the logged-in user's collections. Requires the `write_collections` scope.
954
+ ```
955
+ POST /collections/:collection_id/add
956
+ ```
957
+
958
+ ##### Parameters
959
+
960
+ | Parameter | Type | Description | Optional | Default |
961
+ | --------- | ---- | ----------- | -------- | ------- |
962
+ | **collection_id** | *string* | The collection ID | no | |
963
+ | **photo_id** | *string* | The photo ID | no | |
964
+
965
+ > **Note:** If the photo is already in the collection, this action has no effect.
966
+
967
+ ```ruby
968
+ api.add_photo_to_collection('<collection-id>', '<photo-id>')
969
+ ```
970
+
971
+ #### Remove a Photo from a Collection
972
+ A promise factory to remove a photo from one of the logged-in user's collections. Requires the `write_collections` scope.
973
+ ```
974
+ DELETE /collections/:collection_id/remove
975
+ ```
976
+
977
+ ##### Parameters
978
+
979
+ | Parameter | Type | Description | Optional | Default |
980
+ | --------- | ---- | ----------- | -------- | ------- |
981
+ | **collection_id** | *string* | The collection ID | no | |
982
+ | **photo_id** | *string* | The photo ID | no | |
983
+
984
+ ```ruby
985
+ api.remove_photo_from_collection('<collection-id>', '<photo-id>')
986
+ ```
987
+
988
+ ## Tests
989
+
990
+ RbSplash uses [RSpec](https://rspec.info/) as the testing framework with [WebMock](https://github.com/bblimke/webmock) for HTTP stubbing. Test files are available in the `spec/` folder.
991
+
992
+ ```sh
993
+ bundle exec rspec
994
+ ```
995
+
996
+ ## License
997
+
998
+ The MIT License
999
+
1000
+ Copyright (c) 2026 Sandeep Vattapparambil
1001
+
1002
+ Permission is hereby granted, free of charge, to any person obtaining a copy
1003
+ of this software and associated documentation files (the "Software"), to deal
1004
+ in the Software without restriction, including without limitation the rights
1005
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1006
+ copies of the Software, and to permit persons to whom the Software is
1007
+ furnished to do so, subject to the following conditions:
1008
+
1009
+ The above copyright notice and this permission notice shall be included in
1010
+ all copies or substantial portions of the Software.
1011
+
1012
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1013
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1014
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1015
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1016
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1017
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1018
+ THE SOFTWARE.
1019
+
1020
+ ## Acknowledgements
1021
+
1022
+ Thanks, and Kudos to team [Unsplash](https://unsplash.com/) for creating a wonderful platform for sharing beautiful high quality free images and photos.
1023
+
1024
+ Made with :heart: by [Sandeep Vattapparambil](https://github.com/SandeepVattapparambil).