bookingsync-api 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -1
  3. data/lib/bookingsync/api.rb +1 -0
  4. data/lib/bookingsync/api/client.rb +80 -10
  5. data/lib/bookingsync/api/client/bookings.rb +42 -2
  6. data/lib/bookingsync/api/client/inquiries.rb +23 -0
  7. data/lib/bookingsync/api/client/rentals.rb +3 -2
  8. data/lib/bookingsync/api/error.rb +1 -0
  9. data/lib/bookingsync/api/version.rb +1 -1
  10. data/lib/bookingsync/ext/resource.rb +9 -0
  11. data/spec/bookingsync/api/client/bookings_spec.rb +78 -0
  12. data/spec/bookingsync/api/client/inquiries_spec.rb +37 -0
  13. data/spec/bookingsync/api/client_spec.rb +65 -3
  14. data/spec/cassettes/BookingSync_API_Client_Bookings/_bookings/pagination/with_a_block/yields_block_with_batch_of_bookings.yml +190 -0
  15. data/spec/cassettes/BookingSync_API_Client_Bookings/_bookings/pagination/with_auto_paginate_true/returns_all_bookings_joined_from_many_requests.yml +190 -0
  16. data/spec/cassettes/BookingSync_API_Client_Bookings/_bookings/pagination/with_per_page_setting/returns_limited_number_of_bookings.yml +65 -0
  17. data/spec/cassettes/BookingSync_API_Client_Bookings/_cancel_booking/cancels_given_booking.yml +57 -0
  18. data/spec/cassettes/BookingSync_API_Client_Bookings/_create_booking/creates_a_booking.yml +63 -0
  19. data/spec/cassettes/BookingSync_API_Client_Bookings/_edit_booking/updates_given_booking_by_ID.yml +57 -0
  20. data/spec/cassettes/BookingSync_API_Client_Inquiries/_create_inquiry/creates_a_new_inquiry.yml +64 -0
  21. data/spec/cassettes/BookingSync_API_Client_Inquiries/_inquiries/returns_inquiries.yml +64 -0
  22. data/spec/cassettes/spec/cassettes/BookingSync_API_Client_Bookings/_create_booking/creates_a_booking_yml.yml +62 -0
  23. data/spec/spec_helper.rb +24 -0
  24. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 482e5a2b0789e895be5b708da4e0695d9f0a260f
4
- data.tar.gz: 9243586b0e24bbab147f20ecbce0b6da009de350
3
+ metadata.gz: 43eb6b12130e0e8f634f516be82b68aa771aee94
4
+ data.tar.gz: cb1438eb9863b01ba062f7d695e7f724bb8b5ec1
5
5
  SHA512:
6
- metadata.gz: 366e4825396ef6b9cfcb2596c4ac9fdab053c8b8ce0a82ecfc7c16817724b3995df63eddc5e6e386ce077188b2286f3828fbd5a23875f5f5c82035376a1f4312
7
- data.tar.gz: ae0492b2ab4c56881ba3c225357cd72fd24d981ca94f2e6d602f1236d8914a1e4c0a65e61cfba93efe9312565ac7ebf0eb15d6be2059a4d28e4e3c4e4e72f939
6
+ metadata.gz: b0236f9a753191bb68adc74a0074acd258901ad0f37fd1e5ea78ffa2ce658fe1056482480e1b53abd1cb7dbd0aa6014ed586c45ee4c67114afaae3af1a4411c3
7
+ data.tar.gz: 65eaa193e9b8f89a24a0192486554313f16462494327602966b32efc649eba4e720acb06126d70c175b13e9cf0146a1ba9b82c2455f23885dd4ac3b50b286136
data/README.md CHANGED
@@ -27,7 +27,31 @@ Gem assumes that you already have OAuth token for an account.
27
27
  rentals = api.rentals # => [Sawyer::Resource, Sawyer::Resource]
28
28
  rentals.first.name # => "Small apartment"
29
29
 
30
- See our [documentation](http://rubydoc.info/github/BookingSync/bookingsync-api) for more info.
30
+ ### Pagination
31
+
32
+ All endpoints returning a collection of resources can be paginated. There are three ways to do it.
33
+
34
+ Specify `:per_page` and `:page` params. It's useful when implementing pagination on your site.
35
+
36
+ api.bookings(per_page: 10, page: 1) => [Sawyer::Resource, Sawyer::Resource, ...]
37
+
38
+ Use pagination with a block.
39
+
40
+ api.bookings(per_page: 10) do |batch|
41
+ # display 10 bookings, will make one HTTP request for each batch
42
+ end
43
+
44
+ Fetch all resources (with multiple requests under the hood) and return one big array.
45
+
46
+ api.bookings(auto_paginate: true) => [Sawyer::Resource, Sawyer::Resource, ...]
47
+
48
+ ## Gem documentation
49
+
50
+ See [gem documentation](http://rdoc.info/github/BookingSync/bookingsync-api/master/frames) for more info.
51
+
52
+ ## API documentation
53
+
54
+ See [API documentation](http://docs.api.bookingsync.com).
31
55
 
32
56
  ## Running specs
33
57
 
@@ -1,6 +1,7 @@
1
1
  require "faraday"
2
2
  require "sawyer"
3
3
 
4
+ require "bookingsync/ext/resource"
4
5
  require "bookingsync/api/version"
5
6
  require "bookingsync/api/client"
6
7
 
@@ -1,11 +1,13 @@
1
1
  require "bookingsync/api/middleware/authentication"
2
2
  require "bookingsync/api/client/bookings"
3
+ require "bookingsync/api/client/inquiries"
3
4
  require "bookingsync/api/client/rentals"
4
5
  require "bookingsync/api/error"
5
6
 
6
7
  module BookingSync::API
7
8
  class Client
8
9
  include BookingSync::API::Client::Bookings
10
+ include BookingSync::API::Client::Inquiries
9
11
  include BookingSync::API::Client::Rentals
10
12
 
11
13
  MEDIA_TYPE = "application/vnd.api+json"
@@ -23,9 +25,37 @@ module BookingSync::API
23
25
  # Make a HTTP GET request
24
26
  #
25
27
  # @param path [String] The path, relative to {#api_endpoint}
28
+ # @param options [Hash] Query params for the request
26
29
  # @return [Array<Sawyer::Resource>] Array of resources.
27
30
  def get(path, options = {})
28
- request :get, path, options
31
+ request :get, path, query: options
32
+ end
33
+
34
+ # Make a HTTP POST request
35
+ #
36
+ # @param path [String] The path, relative to {#api_endpoint}
37
+ # @param options [Hash] Body params for the request
38
+ # @return [Array<Sawyer::Resource>]
39
+ def post(path, options = {})
40
+ request :post, path, options
41
+ end
42
+
43
+ # Make a HTTP PUT request
44
+ #
45
+ # @param path [String] The path, relative to {#api_endpoint}
46
+ # @param options [Hash] Body params for the request
47
+ # @return [Array<Sawyer::Resource>]
48
+ def put(path, options = {})
49
+ request :put, path, options
50
+ end
51
+
52
+ # Make a HTTP DELETE request
53
+ #
54
+ # @param path [String] The path, relative to {#api_endpoint}
55
+ # @param options [Hash] Body params for the request
56
+ # @return [Array<Sawyer::Resource>]
57
+ def delete(path, options = {})
58
+ request :delete, path, options
29
59
  end
30
60
 
31
61
  # Return API endpoint
@@ -41,28 +71,68 @@ module BookingSync::API
41
71
  #
42
72
  # @param method [Symbol] HTTP verb to use.
43
73
  # @param path [String] The path, relative to {#api_endpoint}.
44
- # @param query [Hash] A customizable set of query options.
74
+ # @param data [Hash] Data to be send in the request's body
75
+ # it can include query: key with requests params for GET requests
76
+ # @param options [Hash] A customizable set of request options.
45
77
  # @return [Array<Sawyer::Resource>] Array of resources.
46
- def request(method, path, query = {})
47
- [:fields, :status].each do |key|
48
- query[key] = Array(query[key]).join(",") if query.has_key?(key)
78
+ def request(method, path, data, options = {})
79
+ if data.is_a?(Hash)
80
+ options[:query] = data.delete(:query) || {}
81
+ options[:query].keys.each do |key|
82
+ if options[:query][key].is_a?(Array)
83
+ options[:query][key] = options[:query][key].join(",")
84
+ end
85
+ end
49
86
  end
50
87
 
51
- response = agent.call(method, path, nil, {query: query})
88
+ @last_response = response = agent.call(method, path, data.to_json, options)
52
89
  case response.status
53
- # fetch objects from outer hash
54
- # {rentals => [{rental}, {rental}]}
55
- # will return [{rental}, {rental}]
56
- when 200..299; response.data.to_hash.values.flatten
90
+ when 204; [] # update/destroy response
91
+ when 200..299; json_api_to_array(response.data)
57
92
  when 401; raise Unauthorized.new
93
+ when 422; raise UnprocessableEntity.new
58
94
  end
59
95
  end
60
96
 
97
+ def paginate(path, options = {}, &block)
98
+ auto_paginate = options.delete(:auto_paginate)
99
+
100
+ data = request(:get, path, query: options)
101
+
102
+ if (block_given? or auto_paginate) && @last_response.rels[:next]
103
+ first_request = true
104
+ loop do
105
+ if block_given?
106
+ yield(json_api_to_array(@last_response.data))
107
+ elsif auto_paginate
108
+ data.concat(json_api_to_array(@last_response.data)) unless first_request
109
+ first_request = false
110
+ end
111
+ break unless @last_response.rels[:next]
112
+ @last_response = @last_response.rels[:next].get
113
+ end
114
+ end
115
+
116
+ data
117
+ end
118
+
61
119
  private
62
120
 
121
+ # Return collection of resources
122
+ #
123
+ # In jsonapi spec every response has format
124
+ # {resources => [{resource}, {resource}]. This method returns the inner Array
125
+ # @param data [Sawyer::Resource] Sawyer resource from response.data
126
+ # @return [<Sawyer::Resource>] An Array of resources
127
+ # FIXME: This could have better name
128
+ def json_api_to_array(data)
129
+ data.to_hash.values.flatten
130
+ end
131
+
63
132
  def agent
64
133
  @agent ||= Sawyer::Agent.new(api_endpoint, sawyer_options) do |http|
65
134
  http.headers[:accept] = MEDIA_TYPE
135
+ http.headers[:content_type] = MEDIA_TYPE
66
136
  end
67
137
  end
68
138
 
@@ -15,11 +15,51 @@ module BookingSync::API
15
15
  # are shown, otherwise they are hidden.
16
16
  # @option options [Array] status: Array of booking states.
17
17
  # If specyfied bookings with given states are shown.
18
+ # Possible statuses: `:booked`, `:unavailable` and `:tentative`
18
19
  # @return [Array<Sawyer::Resource>] Array of bookings.
19
20
  # @example
20
21
  # @api.bookings(months: 12, states: [:booked, :unavailable], include_canceled: true)
21
- def bookings(options = {})
22
- get :bookings, options
22
+ #
23
+ # @example Pagination
24
+ # @api.bookings(per_page: 10) do |batch|
25
+ # # do something with ten bookings
26
+ # end
27
+ # @see http://docs.api.bookingsync.com/reference/endpoints/bookings/#list-bookings
28
+ # @see http://docs.api.bookingsync.com/reference/endpoints/bookings/#search-bookings
29
+ def bookings(options = {}, &block)
30
+ paginate :bookings, options, &block
31
+ end
32
+
33
+
34
+ # Create a booking
35
+ #
36
+ # @param options [Hash] Booking attributes
37
+ # @return <Sawyer::Resource> Newly create booking
38
+ def create_booking(options = {})
39
+ post(:bookings, bookings: [options]).pop
40
+ end
41
+
42
+ # Edit a booking
43
+ #
44
+ # @param booking [Sawyer::Resource|Integer] Booking or ID of the booking
45
+ # to be updated
46
+ # @param options [Hash] Booking attributes to be updated
47
+ # FIXME: should be changed resource
48
+ # @return [Array] An empty Array on success, exception is raised otherwise
49
+ # @example
50
+ # booking = @api.bookings.first
51
+ # @api.edit_booking(booking, {adults: 1}) => []
52
+ def edit_booking(booking, options = {})
53
+ put "bookings/#{booking}", bookings: [options]
54
+ end
55
+
56
+ # Cancel a booking
57
+ #
58
+ # @param booking [Sawyer::Resource|Integer] Booking or ID of the booking
59
+ # to be canceled
60
+ # @return [Array] An empty Array on success, exception is raised otherwise
61
+ def cancel_booking(booking, options = {})
62
+ delete "bookings/#{booking}"
23
63
  end
24
64
  end
25
65
  end
@@ -0,0 +1,23 @@
1
+ module BookingSync::API
2
+ class Client
3
+ module Inquiries
4
+ # List inquiries
5
+ #
6
+ # Return list of inquiries for current account.
7
+ # @param options [Hash] A customizable set of query options.
8
+ # @return [Array<Sawyer::Resource>] Array of inquiries.
9
+ # @see http://docs.api.bookingsync.com/reference/endpoints/inquiries/#list-inquiries
10
+ def inquiries(options = {}, &block)
11
+ paginate :inquiries, options, &block
12
+ end
13
+
14
+ # Create a new inquiry
15
+ #
16
+ # @param options [Hash] Inquiry attributes
17
+ # @return <Sawyer::Resource> Newly create inquiry
18
+ def create_inquiry(options = {})
19
+ post(:inquiries, inquiries: [options]).pop
20
+ end
21
+ end
22
+ end
23
+ end
@@ -13,8 +13,9 @@ module BookingSync::API
13
13
  # rentals.first.name # => "Small apartment"
14
14
  # @example Get the list of rentals only with name and description for smaller response
15
15
  # @api.rentals(fields: [:name, :description])
16
- def rentals(options = {})
17
- get :rentals, options
16
+ # @see http://docs.api.bookingsync.com/reference/endpoints/rentals/#list-rentals
17
+ def rentals(options = {}, &block)
18
+ paginate :rentals, options, &block
18
19
  end
19
20
  end
20
21
  end
@@ -2,4 +2,5 @@ module BookingSync::API
2
2
  # Class for rescuing all BS API errors
3
3
  class Error < StandardError; end
4
4
  class Unauthorized < Error; end
5
+ class UnprocessableEntity < Error; end
5
6
  end
@@ -1,5 +1,5 @@
1
1
  module BookingSync
2
2
  module API
3
- VERSION = "0.0.4"
3
+ VERSION = "0.0.5"
4
4
  end
5
5
  end
@@ -0,0 +1,9 @@
1
+ module Sawyer
2
+ class Resource
3
+ # With below alias it's possible to pass ID or Sawyer::Resource
4
+ # to edit/destroy methods
5
+ def to_s
6
+ id
7
+ end
8
+ end
9
+ end
@@ -8,5 +8,83 @@ describe BookingSync::API::Client::Bookings do
8
8
  expect(client.bookings).not_to be_nil
9
9
  assert_requested :get, bs_url("bookings")
10
10
  end
11
+
12
+ describe "pagination" do
13
+ context "with per_page setting" do
14
+ it "returns limited number of bookings" do
15
+ bookings = client.bookings(per_page: 2)
16
+ expect(bookings.size).to eql(2)
17
+ end
18
+ end
19
+
20
+ context "with a block" do
21
+ it "yields block with batch of bookings" do
22
+ sizes = [2, 2, 1]
23
+ index = 0
24
+ client.bookings(per_page: 2) do |bookings|
25
+ expect(bookings.size).to eql(sizes[index])
26
+ index += 1
27
+ end
28
+ end
29
+ end
30
+
31
+ context "with auto_paginate: true" do
32
+ it "returns all bookings joined from many requests" do
33
+ bookings = client.bookings(per_page: 2, auto_paginate: true)
34
+ expect(bookings.size).to eql(5)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ describe ".create_booking", :vcr do
41
+ let(:attributes) {
42
+ {start_at: '2014-01-03', end_at: '2014-01-04', rental_id: 20,
43
+ booked: true}
44
+ }
45
+
46
+ it "creates a booking" do
47
+ client.create_booking(attributes)
48
+ assert_requested :post, bs_url("bookings"),
49
+ body: {bookings: [attributes]}.to_json
50
+ end
51
+
52
+ it "returns newly created booking" do
53
+ VCR.use_cassette('BookingSync_API_Client_Bookings/_create_booking/creates_a_booking') do
54
+ booking = client.create_booking(attributes)
55
+ expect(booking.account_id).to eql(1)
56
+ expect(booking.rental_id).to eql(20)
57
+ end
58
+ end
59
+ end
60
+
61
+ describe ".edit_booking", :vcr do
62
+ it "updates given booking by ID" do
63
+ client.edit_booking(50, {adults: 1})
64
+ assert_requested :put, bs_url("bookings/50"),
65
+ body: {bookings: [{adults: 1}]}.to_json
66
+ end
67
+
68
+ it "returns an empty array" do
69
+ VCR.use_cassette('BookingSync_API_Client_Bookings/_edit_booking/updates_given_booking_by_ID') do
70
+ expect(client.edit_booking(50, {adults: 1})).to eql([])
71
+ end
72
+ end
73
+
74
+ it "updates given booking by Resource object" do
75
+ VCR.use_cassette('BookingSync_API_Client_Bookings/_edit_booking/updates_given_booking_by_ID') do
76
+ resource = double(to_s: "50")
77
+ client.edit_booking(resource, {adults: 1})
78
+ assert_requested :put, bs_url("bookings/50"),
79
+ body: {bookings: [{adults: 1}]}.to_json
80
+ end
81
+ end
82
+ end
83
+
84
+ describe ".cancel_booking", :vcr do
85
+ it "cancels given booking" do
86
+ client.cancel_booking(50)
87
+ assert_requested :delete, bs_url("bookings/50")
88
+ end
11
89
  end
12
90
  end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe BookingSync::API::Client::Inquiries do
4
+ let(:client) { BookingSync::API::Client.new(test_access_token) }
5
+
6
+ describe ".inquiries", :vcr do
7
+ it "returns inquiries" do
8
+ expect(client.inquiries).not_to be_nil
9
+ assert_requested :get, bs_url("inquiries")
10
+ end
11
+ end
12
+
13
+ describe ".create_inquiry", :vcr do
14
+ let(:attributes) { {
15
+ rental_id: 7,
16
+ start_at: Time.now,
17
+ end_at: Time.now + 86400, # one day
18
+ firstname: "John",
19
+ lastname: "Smith",
20
+ email: "john@example.com"
21
+ } }
22
+
23
+ it "creates a new inquiry" do
24
+ client.create_inquiry(attributes)
25
+ assert_requested :post, bs_url("inquiries"),
26
+ body: {inquiries: [attributes]}.to_json
27
+ end
28
+
29
+ it "returns newly created inquiry" do
30
+ VCR.use_cassette('BookingSync_API_Client_Inquiries/_create_inquiry/creates_a_new_inquiry') do
31
+ inquiry = client.create_inquiry(attributes)
32
+ expect(inquiry.rental_id).to eql(7)
33
+ expect(inquiry.firstname).to eql("John")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -17,6 +17,48 @@ describe BookingSync::API::Client do
17
17
  client.get("resource")
18
18
  assert_requested :get, bs_url("resource")
19
19
  end
20
+
21
+ it "adds query options to the URL" do
22
+ stub_get("resource?abc=123")
23
+ client.get("resource", abc: 123)
24
+ assert_requested :get, bs_url("resource?abc=123")
25
+ end
26
+ end
27
+
28
+ describe "#post" do
29
+ before { VCR.turn_off! }
30
+ it "makes a HTTP POST request with body" do
31
+ stub_post("resource")
32
+ client.post("resource", {key: :value})
33
+ assert_requested :post, bs_url("resource"), body: '{"key":"value"}'
34
+ end
35
+
36
+ context "on 422 response" do
37
+ it "raises UnprocessableEntity exception" do
38
+ stub_post("resource", status: 422)
39
+ expect {
40
+ client.post("resource", {key: :value})
41
+ }.to raise_error(BookingSync::API::UnprocessableEntity)
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "#put" do
47
+ before { VCR.turn_off! }
48
+ it "makes a HTTP PUT request with body" do
49
+ stub_put("resource")
50
+ client.put("resource", {key: :value})
51
+ assert_requested :put, bs_url("resource"), body: '{"key":"value"}'
52
+ end
53
+ end
54
+
55
+ describe "#delete" do
56
+ before { VCR.turn_off! }
57
+ it "makes a HTTP DELETE request" do
58
+ stub_delete("resource")
59
+ client.delete("resource")
60
+ assert_requested :delete, bs_url("resource")
61
+ end
20
62
  end
21
63
 
22
64
  describe "#request" do
@@ -29,13 +71,20 @@ describe BookingSync::API::Client do
29
71
  headers: {"Authorization" => "Bearer fake-access-token"}
30
72
  end
31
73
 
32
- it "requests proper content type for JSON API" do
74
+ it "requests proper accept header for JSON API" do
33
75
  stub_get("resource")
34
76
  client.get("resource")
35
77
  assert_requested :get, bs_url("resource"),
36
78
  headers: {"Accept" => "application/vnd.api+json"}
37
79
  end
38
80
 
81
+ it "requests sends data with JSON API content type" do
82
+ stub_post("resource")
83
+ client.post("resource")
84
+ assert_requested :post, bs_url("resource"),
85
+ headers: {"Content-Type" => "application/vnd.api+json"}
86
+ end
87
+
39
88
  it "returns Array of resources" do
40
89
  stub_get("resource", body: {resources: [{name: "Megan"}]}.to_json)
41
90
  resources = client.get("resource")
@@ -43,7 +92,7 @@ describe BookingSync::API::Client do
43
92
  expect(resources.first.name).to eq("Megan")
44
93
  end
45
94
 
46
- context "client returns 401" do
95
+ context "API returns 401" do
47
96
  it "raises Unauthorized exception" do
48
97
  stub_get("resource", status: 401)
49
98
  expect {
@@ -52,19 +101,32 @@ describe BookingSync::API::Client do
52
101
  end
53
102
  end
54
103
 
55
- context "status code is outside 200..299 range" do
104
+ context "API returns status code outside 200..299 range" do
56
105
  it "returns nil" do
57
106
  stub_get("resource", status: 404)
58
107
  expect(client.get("resource")).to be_nil
59
108
  end
60
109
  end
61
110
 
111
+ context "API returns 204 No Content" do
112
+ it "returns an empty array" do
113
+ stub_get("resource", status: 204)
114
+ expect(client.get("resource")).to eql([])
115
+ end
116
+ end
117
+
62
118
  context "user wants to fetch only specific fields" do
63
119
  it "constructs url for filtered fields" do
64
120
  stub_get("resource?fields=name,description")
65
121
  client.get("resource", fields: [:name, :description])
66
122
  assert_requested :get, bs_url("resource?fields=name,description")
67
123
  end
124
+
125
+ it "should support single field" do
126
+ stub_get("resource?fields=name")
127
+ client.get("resource", fields: :name)
128
+ assert_requested :get, bs_url("resource?fields=name")
129
+ end
68
130
  end
69
131
 
70
132
  context "user passes additional query options" do