camper 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 984eef67b2149dac0f1735f6dcda5012b140dd4d3404b8523e23b21096d510de
4
- data.tar.gz: 90b228ed0f5388a6866346c46b98a608d44a8753bec6a7eef844cd85583afd34
3
+ metadata.gz: b7d0cdad6f722b4f3c02ba3da38147000e371f57860ce203fb1c64eb007797de
4
+ data.tar.gz: db77821cf2cb77070b049dce6cbf4bc62430c4948101aadf7d2c87b4e67eaa42
5
5
  SHA512:
6
- metadata.gz: 6bac9cf265b6ec6ddcdaa38fb6290e9c35b16c8a4dc1c64446589eea095dd2a4dcda4a491b0e46c20c9aad45e6af4d8168b693dcfc556d125b78fc8de35caf7a
7
- data.tar.gz: c605925535d3052d2844d2fe7ac0ee2b420f2891c78e1e14422bb3e8cf9464399eb741dbdde2fd7611f49743de2097f8a62f06e98e0419d2dc4298551328b604
6
+ metadata.gz: '027558808a97c4ef3c04c6ec895c86a8975e32f7a03397e882513db25f0cfb29978135c23a77a87081cfa845834c31419e7813c24a3bf43aeaf030b777e31c28'
7
+ data.tar.gz: 7aa221c2f13009488cc57d46fc86e1daeb21e2051b70e33dc6780032b8091bfdcb9f9efd54286b774b2f9406bd78c889add255e47ef674870aae07887c190ac5
@@ -9,7 +9,7 @@ AllCops:
9
9
  - 'camper.gemspec'
10
10
 
11
11
  Layout/LineLength:
12
- Max: 123
12
+ Max: 120
13
13
  Exclude:
14
14
  - 'lib/camper/client/*'
15
15
  - 'spec/**/*'
@@ -18,6 +18,9 @@ Metrics/BlockLength:
18
18
  Exclude:
19
19
  - 'spec/**/*'
20
20
 
21
+ Metrics/AbcSize:
22
+ Max: 20
23
+
21
24
  Style/Documentation:
22
25
  Enabled: false
23
26
 
@@ -0,0 +1,4 @@
1
+ --no-private
2
+ lib/**/*.rb
3
+ examples/*.rb -
4
+ README.md CONTRIBUTING.md CHANGELOG.md
@@ -1,16 +1,36 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased](https://github.com/renehernandez/camper/tree/HEAD)
3
+ ## [v0.0.6](https://github.com/renehernandez/camper/tree/v0.0.6) (2020-10-01)
4
+
5
+ **Implemented enhancements:**
6
+
7
+ - Implement handling error according to Basecamp 3 API specifications [\#21](https://github.com/renehernandez/camper/issues/21)
8
+ - Add ability to complete a Todo [\#12](https://github.com/renehernandez/camper/issues/12)
9
+ - Multiple improvements [\#38](https://github.com/renehernandez/camper/pull/38)
10
+ - Error handling improvements [\#37](https://github.com/renehernandez/camper/pull/37)
11
+
12
+ **Documentation:**
13
+
14
+ - Enable documentation in RubyDoc [\#6](https://github.com/renehernandez/camper/issues/6)
15
+ - Add yard documentation [\#41](https://github.com/renehernandez/camper/pull/41)
16
+ - Fix gem badge [\#39](https://github.com/renehernandez/camper/pull/39)
17
+
18
+ **Merged pull requests:**
19
+
20
+ - Rename add\_comment to create\_comment [\#40](https://github.com/renehernandez/camper/pull/40)
21
+ - Bump rubocop from 0.91.0 to 0.92.0 [\#36](https://github.com/renehernandez/camper/pull/36)
22
+
23
+ ## [v0.0.5](https://github.com/renehernandez/camper/tree/v0.0.5) (2020-09-22)
4
24
 
5
25
  **Implemented enhancements:**
6
26
 
7
27
  - Enable dependabot [\#22](https://github.com/renehernandez/camper/pull/22)
8
- - Retry for new access token [\#16](https://github.com/renehernandez/camper/pull/16)
9
28
 
10
29
  **Fixed bugs:**
11
30
 
12
31
  - Implement pagination according to basecamp 3 API [\#20](https://github.com/renehernandez/camper/issues/20)
13
32
  - Implement pagination according to Basecamp 3 API [\#26](https://github.com/renehernandez/camper/pull/26)
33
+ - Remove unreleasedLabel field [\#15](https://github.com/renehernandez/camper/pull/15)
14
34
 
15
35
  **Merged pull requests:**
16
36
 
@@ -37,10 +57,7 @@
37
57
  **Implemented enhancements:**
38
58
 
39
59
  - Request a new access token once it expires [\#13](https://github.com/renehernandez/camper/issues/13)
40
-
41
- **Fixed bugs:**
42
-
43
- - Remove unreleasedLabel field [\#15](https://github.com/renehernandez/camper/pull/15)
60
+ - Retry for new access token [\#16](https://github.com/renehernandez/camper/pull/16)
44
61
 
45
62
  **Documentation:**
46
63
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- camper (0.0.4)
4
+ camper (0.0.5)
5
5
  httparty (~> 0.18)
6
6
  rack-oauth2 (~> 1.14)
7
7
 
@@ -38,7 +38,7 @@ GEM
38
38
  minitest (5.14.2)
39
39
  multi_xml (0.6.0)
40
40
  parallel (1.19.2)
41
- parser (2.7.1.4)
41
+ parser (2.7.1.5)
42
42
  ast (~> 2.4.1)
43
43
  pry (0.13.1)
44
44
  coderay (~> 1.1)
@@ -67,25 +67,28 @@ GEM
67
67
  diff-lcs (>= 1.2.0, < 2.0)
68
68
  rspec-support (~> 3.9.0)
69
69
  rspec-support (3.9.3)
70
- rubocop (0.91.0)
70
+ rubocop (0.92.0)
71
71
  parallel (~> 1.10)
72
- parser (>= 2.7.1.1)
72
+ parser (>= 2.7.1.5)
73
73
  rainbow (>= 2.2.2, < 4.0)
74
74
  regexp_parser (>= 1.7)
75
75
  rexml
76
- rubocop-ast (>= 0.4.0, < 1.0)
76
+ rubocop-ast (>= 0.5.0)
77
77
  ruby-progressbar (~> 1.7)
78
78
  unicode-display_width (>= 1.4.0, < 2.0)
79
- rubocop-ast (0.4.2)
80
- parser (>= 2.7.1.4)
79
+ rubocop-ast (0.7.0)
80
+ parser (>= 2.7.1.5)
81
+ strscan (>= 1.0.0)
81
82
  rubocop-performance (1.8.1)
82
83
  rubocop (>= 0.87.0)
83
84
  rubocop-ast (>= 0.4.0)
84
85
  ruby-progressbar (1.10.1)
86
+ strscan (1.0.3)
85
87
  thread_safe (0.3.6)
86
88
  tzinfo (1.2.7)
87
89
  thread_safe (~> 0.1)
88
90
  unicode-display_width (1.7.0)
91
+ yard (0.9.25)
89
92
  zeitwerk (2.4.0)
90
93
 
91
94
  PLATFORMS
@@ -98,6 +101,7 @@ DEPENDENCIES
98
101
  rspec (~> 3.9)
99
102
  rubocop
100
103
  rubocop-performance
104
+ yard (~> 0.9)
101
105
 
102
106
  BUNDLED WITH
103
107
  2.1.4
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
- # camper ![CI](https://github.com/renehernandez/camper/workflows/CI/badge.svg) [![Gem Version](https://badge.fury.io/rb/Camper.svg)](https://badge.fury.io/rb/camper)
1
+ # camper ![CI](https://github.com/renehernandez/camper/workflows/CI/badge.svg) [![Gem Version](https://badge.fury.io/rb/camper.svg)](https://badge.fury.io/rb/camper)
2
2
 
3
3
  Camper is a Ruby wrapper for the [Basecamp 3 API](https://github.com/basecamp/bc3-api).
4
4
 
5
+ You can check out the gem documentation at [https://www.rubydoc.org/gems/camper](https://www.rubydoc.org/gems/camper)
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -24,7 +26,9 @@ $ gem install camper
24
26
 
25
27
  ## Usage
26
28
 
27
- Getting a client and configuring it:
29
+ ### Configuration
30
+
31
+ Getting a `client` and configuring it:
28
32
 
29
33
  ```ruby
30
34
  require 'camper'
@@ -32,44 +36,58 @@ require 'camper'
32
36
  client = Camper.client
33
37
 
34
38
  client.configure do |config|
35
- config.client_id = ENV['BASEcamper_CLIENT_ID']
36
- config.client_secret = ENV['BASEcamper_CLIENT_SECRET']
37
- config.account_number = ENV['BASEcamper_ACCOUNT_NUMBER']
38
- config.refresh_token = ENV['BASEcamper_REFRESH_TOKEN']
39
- config.access_token = ENV['BASEcamper_ACCESS_TOKEN']
39
+ config.client_id = 'client_id'
40
+ config.client_secret = 'client_secret'
41
+ config.account_number = 'account_number'
42
+ config.refresh_token = 'refresh_token'
43
+ config.access_token = 'access_token'
40
44
  end
41
-
42
- projects = client.projects
43
45
  ```
44
46
 
45
- Alternatively, it is possible to invoke the top-level `#configure` method to get a client:
47
+ Alternatively, it is possible to invoke the top-level `#configure` method to get a `client`:
46
48
 
47
49
  ```ruby
48
50
  require 'camper'
49
51
 
50
52
  client = Camper.configure do |config|
51
- config.client_id = ENV['BASEcamper_CLIENT_ID']
52
- config.client_secret = ENV['BASEcamper_CLIENT_SECRET']
53
- config.account_number = ENV['BASEcamper_ACCOUNT_NUMBER']
54
- config.refresh_token = ENV['BASEcamper_REFRESH_TOKEN']
55
- config.access_token = ENV['BASEcamper_ACCESS_TOKEN']
53
+ config.client_id = 'client_id'
54
+ config.client_secret = 'client_secret'
55
+ config.account_number = 'account_number'
56
+ config.refresh_token = 'refresh_token'
57
+ config.access_token = 'access_token'
56
58
  end
59
+ ```
57
60
 
58
- # gets a paginated response
59
- projects = client.projects
61
+ Also, the `client` can read directly the following environment variables:
62
+
63
+ * `BASECAMP_CLIENT_ID`
64
+ * `BASECAMP_CLIENT_SECRET`
65
+ * `BASECAMP_ACCOUNT_NUMBER`
66
+ * `BASECAMP_REFRESH_TOKEN`
67
+ * `BASECAMP_ACCESS_TOKEN`
68
+
69
+ then the code would look like:
70
+
71
+ ```ruby
72
+ require 'camper'
73
+
74
+ client = Camper.client
60
75
  ```
61
76
 
77
+
78
+ ### Examples
79
+
62
80
  Example getting list of TODOs:
63
81
 
64
82
  ```ruby
65
83
  require 'camper'
66
84
 
67
85
  client = Camper.configure do |config|
68
- config.client_id = ENV['BASEcamper_CLIENT_ID']
69
- config.client_secret = ENV['BASEcamper_CLIENT_SECRET']
70
- config.account_number = ENV['BASEcamper_ACCOUNT_NUMBER']
71
- config.refresh_token = ENV['BASEcamper_REFRESH_TOKEN']
72
- config.access_token = ENV['BASEcamper_ACCESS_TOKEN']
86
+ config.client_id = ENV['BASECAMP_CLIENT_ID']
87
+ config.client_secret = ENV['BASECAMP_CLIENT_SECRET']
88
+ config.account_number = ENV['BASECAMP_ACCOUNT_NUMBER']
89
+ config.refresh_token = ENV['BASECAMP_REFRESH_TOKEN']
90
+ config.access_token = ENV['BASECAMP_ACCESS_TOKEN']
73
91
  end
74
92
 
75
93
  # gets a paginated response
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_development_dependency 'rake', '~> 13.0'
33
33
  spec.add_development_dependency 'rspec', '~> 3.9'
34
+ spec.add_development_dependency 'yard', '~> 0.9'
34
35
  end
@@ -25,7 +25,7 @@ projects.auto_paginate do |p|
25
25
  # Adds a comment on the first todolist
26
26
  list = client.todolists(todoset).first
27
27
  puts "Todolist: #{list.title}, can be commented on: #{list.can_be_commented?}"
28
- client.add_comment(list, 'New <b>comment</b> with <i>HTML support</i>')
28
+ client.create_comment(list, 'New <b>comment</b> with <i>HTML support</i>')
29
29
  comments = client.comments(list)
30
30
  idx = 0
31
31
  comments.auto_paginate do |c|
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'camper'
4
+
5
+ # It will configure the client using the basecamp environment variables
6
+ client = Camper.client
7
+
8
+ project = client.project(ENV['PROJECT_ID'])
9
+
10
+ todoset = client.todoset(project)
11
+
12
+ todolist = client.todolist(todoset, ENV['TODOLIST_ID'])
13
+
14
+ puts todolist.title
15
+
16
+ client.todos(todolist).auto_paginate do |todo|
17
+ puts todo.title
18
+ end
19
+
20
+ todo = client.create_todo(todolist, 'TODO from camper', description: 'This is a todo created with camper')
21
+
22
+ puts todo.title
23
+
24
+ client.complete_todo(todo)
@@ -2,8 +2,8 @@
2
2
 
3
3
  class Camper::Client
4
4
  module CommentAPI
5
- def add_comment(resource, content)
6
- post(resource.comments_url, override_path: true, body: { content: content }.to_json)
5
+ def create_comment(resource, content)
6
+ post(resource.comments_url, override_path: true, body: { content: content })
7
7
  end
8
8
 
9
9
  def comments(resource)
@@ -7,6 +7,10 @@ class Camper::Client
7
7
  get("/projects", options)
8
8
  end
9
9
 
10
+ def project(id)
11
+ get("/projects/#{id}")
12
+ end
13
+
10
14
  def message_board(project)
11
15
  board = project.message_board
12
16
  get(board.url, override_path: true)
@@ -2,10 +2,8 @@
2
2
 
3
3
  class Camper::Client
4
4
  module ResourceAPI
5
-
6
5
  def resource(url)
7
- get(url_transform(url), override_path: true)
6
+ get(url, override_path: true)
8
7
  end
9
-
10
8
  end
11
9
  end
@@ -3,12 +3,78 @@
3
3
  class Camper::Client
4
4
  module TodoAPI
5
5
 
6
- def todolists(todoset)
7
- get(todoset.todolists_url, override_path: true)
6
+ # Get the todolists associated with the todoset
7
+ #
8
+ # @example
9
+ # client.todolists(todoset)
10
+ # @example
11
+ # client.todolists(todoset, status: 'archived')
12
+ #
13
+ # @param todoset [Resource] the parent todoset resource
14
+ # @param options [Hash] extra options to filter the list of todolist
15
+ # @return [Array<Resource>]
16
+ # @see https://github.com/basecamp/bc3-api/blob/master/sections/todolists.md#get-to-do-lists
17
+ def todolists(todoset, options={})
18
+ get(todoset.todolists_url, options.merge(override_path: true))
8
19
  end
9
20
 
21
+ # Get a todolist with a given id
22
+ #
23
+ # @example
24
+ # client.todolist(todoset, '2345')
25
+ #
26
+ # @param todoset [Resource] the parent todoset resource
27
+ # @param id [Integer, String] the id of the todolist to get
28
+ # @return [Resource]
29
+ # @see https://github.com/basecamp/bc3-api/blob/master/sections/todolists.md#get-a-to-do-list
30
+ def todolist(todoset, id)
31
+ get("/buckets/#{todoset.bucket.id}/todolists/#{id}")
32
+ end
33
+
34
+ # Get the todos in a todolist
35
+ #
36
+ # @example
37
+ # client.todos(todolist)
38
+ # @example
39
+ # client.todos(todolist, completed: true)
40
+ #
41
+ # @param todolist [Resource] the parent todoset resource
42
+ # @param options [Hash] options to filter the list of todos
43
+ # @return [Resource]
44
+ # @see https://github.com/basecamp/bc3-api/blob/master/sections/todos.md#get-to-dos
10
45
  def todos(todolist, options={})
11
46
  get(todolist.todos_url, options.merge(override_path: true))
12
47
  end
48
+
49
+ # Create a todo within a todolist
50
+ #
51
+ # @example
52
+ # client.create_todo(todolist, 'First Todo')
53
+ # @example
54
+ # client.create_todo(
55
+ # todolist,
56
+ # 'Program it',
57
+ # description: "<div><em>Try that new language!</em></div>, due_on: "2016-05-01"
58
+ # )
59
+ #
60
+ # @param todolist [Resource] the todolist where the todo is going to be created
61
+ # @param content [String] what the to-do is for
62
+ # @param options [Hash] extra configuration for the todo such as due_date and description
63
+ # @return [Resource]
64
+ # @see https://github.com/basecamp/bc3-api/blob/master/sections/todos.md#create-a-to-do
65
+ def create_todo(todolist, content, options={})
66
+ post(todolist.todos_url, body: { content: content, **options }, override_path: true)
67
+ end
68
+
69
+ # Complete a todo
70
+ #
71
+ # @example
72
+ # client.complete_todo(todo)
73
+ #
74
+ # @param todo [Resource] the todo to be marked as completed
75
+ # @see https://github.com/basecamp/bc3-api/blob/master/sections/todos.md#complete-a-to-do
76
+ def complete_todo(todo)
77
+ post("#{todo.url}/completion", override_path: true)
78
+ end
13
79
  end
14
80
  end
@@ -38,7 +38,7 @@ module Camper
38
38
 
39
39
  def update_access_token!
40
40
  logger.debug "Update access token using refresh token"
41
-
41
+
42
42
  client = authz_client
43
43
  client.refresh_token = @config.refresh_token
44
44
 
@@ -6,7 +6,7 @@ module Camper
6
6
  Dir[File.expand_path('api/*.rb', __dir__)].each { |f| require f }
7
7
 
8
8
  extend Forwardable
9
-
9
+
10
10
  def_delegators :@config, *(Configuration::VALID_OPTIONS_KEYS)
11
11
  def_delegators :@config, :authz_endpoint, :token_endpoint, :api_endpoint, :base_api_endpoint
12
12
 
@@ -19,7 +19,7 @@ module Camper
19
19
  include ResourceAPI
20
20
  include TodoAPI
21
21
 
22
- # Creates a new API.
22
+ # Creates a new Client instance.
23
23
  # @raise [Error:MissingCredentials]
24
24
  def initialize(options = {})
25
25
  @config = Configuration.new(options)
@@ -27,18 +27,19 @@ module Camper
27
27
 
28
28
  %w[get post put delete].each do |method|
29
29
  define_method method do |path, options = {}|
30
- response, result = new_request.send(method, path, options)
31
- return response unless result == Request::Result::AccessTokenExpired
32
-
33
- update_access_token!
30
+ request = new_request(method, path, options)
34
31
 
35
- response, = new_request.send(method, path, options)
36
- response
32
+ loop do
33
+ response, result = request.execute
34
+ logger.debug("Request result: #{result}; Attempt: #{request.attempts}")
35
+ return response unless retry_request?(response, result)
36
+ end
37
37
  end
38
38
  end
39
39
 
40
40
  # Allows setting configuration values for this client
41
- # returns the client instance being configured
41
+ # by yielding the config object to the block
42
+ # @return [Camper::Client] the client instance being configured
42
43
  def configure
43
44
  yield @config
44
45
 
@@ -54,33 +55,34 @@ module Camper
54
55
  inspected
55
56
  end
56
57
 
57
- # Utility method for URL encoding of a string.
58
- # Copied from https://ruby-doc.org/stdlib-2.7.0/libdoc/erb/rdoc/ERB/Util.html
59
- #
60
- # @return [String]
61
- def url_encode(url)
62
- url.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/n) { |m| sprintf('%%%02X', m.unpack1('C')) } # rubocop:disable Style/FormatString, Style/FormatStringToken
58
+ private
59
+
60
+ def new_request(method, path, options)
61
+ Request.new(self, method, path, options)
63
62
  end
64
63
 
65
- private
64
+ def retry_request?(response, result)
65
+ case result
66
+ when Request::Result::ACCESS_TOKEN_EXPIRED
67
+ update_access_token!
68
+ true
69
+ when Request::Result::TOO_MANY_REQUESTS
70
+ sleep_before_retrying(response)
71
+ true
72
+ else
73
+ false
74
+ end
75
+ end
76
+
77
+ def sleep_before_retrying(response)
78
+ time = response.headers['Retry-After'].to_i
79
+ logger.debug("Sleeping for #{time} seconds before retrying request")
66
80
 
67
- def new_request
68
- Request.new(@config.access_token, @config.user_agent, self)
81
+ sleep(time)
69
82
  end
70
83
 
71
84
  def only_show_last_four_chars(token)
72
85
  "#{'*' * (token.size - 4)}#{token[-4..-1]}"
73
86
  end
74
-
75
- # Utility method for transforming Basecamp Web URLs into API URIs
76
- # e.g 'https://3.basecamp.com/1/buckets/2/todos/3' will be
77
- # converted into 'https://3.basecampapi.com/1/buckets/2/todos/3.json'
78
- #
79
- # @return [String]
80
- def url_transform(url)
81
- api_uri = url.gsub('3.basecamp.com', '3.basecampapi.com')
82
- api_uri += '.json' unless url.end_with? '.json'
83
- api_uri
84
- end
85
87
  end
86
88
  end
@@ -23,7 +23,7 @@ module Camper
23
23
  attr_accessor(*VALID_OPTIONS_KEYS)
24
24
 
25
25
  def initialize(options = {})
26
- options[:user_agent] ||= DEFAULT_USER_AGENT
26
+ default_from_environment
27
27
  VALID_OPTIONS_KEYS.each do |key|
28
28
  send("#{key}=", options[key]) if options[key]
29
29
  end
@@ -36,10 +36,9 @@ module Camper
36
36
  end
37
37
  end
38
38
 
39
- # rubocop:disable Metrics/AbcSize
40
39
  # Resets all configuration options to the defaults.
41
- def reset
42
- logger.debug 'Resetting attributes to default environment values'
40
+ def default_from_environment
41
+ logger.debug 'Setting attributes to default environment values'
43
42
  self.client_id = ENV['BASECAMP3_CLIENT_ID']
44
43
  self.client_secret = ENV['BASECAMP3_CLIENT_SECRET']
45
44
  self.redirect_uri = ENV['BASECAMP3_REDIRECT_URI']
@@ -48,7 +47,6 @@ module Camper
48
47
  self.access_token = ENV['BASECAMP3_ACCESS_TOKEN']
49
48
  self.user_agent = ENV['BASECAMP3_USER_AGENT'] || DEFAULT_USER_AGENT
50
49
  end
51
- # rubocop:enable Metrics/AbcSize
52
50
 
53
51
  def authz_endpoint
54
52
  'https://launchpad.37signals.com/authorization/new'
@@ -60,7 +58,7 @@ module Camper
60
58
 
61
59
  def api_endpoint
62
60
  raise Camper::Error::InvalidConfiguration, "missing basecamp account" unless self.account_number
63
-
61
+
64
62
  "#{self.base_api_endpoint}/#{self.account_number}"
65
63
  end
66
64
 
@@ -11,9 +11,14 @@ module Camper
11
11
  # Raised when API endpoint credentials not configured.
12
12
  class MissingCredentials < Error; end
13
13
 
14
+ class MissingBody < Error; end
15
+
14
16
  # Raised when impossible to parse response body.
15
17
  class Parsing < Error; end
16
18
 
19
+ # Raised when too many attempts for the same request
20
+ class TooManyRetries < Error; end
21
+
17
22
  # Custom error class for rescuing from HTTP response errors.
18
23
  class ResponseError < Error
19
24
  POSSIBLE_MESSAGE_KEYS = %i[message error_description error].freeze
@@ -70,9 +75,6 @@ module Camper
70
75
  # Return stringified response when receiving a
71
76
  # parsing error to avoid obfuscation of the
72
77
  # api error.
73
- #
74
- # note: The Camper API does not always return valid
75
- # JSON when there are errors.
76
78
  @response.to_s
77
79
  end
78
80
 
@@ -127,6 +129,8 @@ module Camper
127
129
  # Raised when API endpoint returns the HTTP status code 503.
128
130
  class ServiceUnavailable < ResponseError; end
129
131
 
132
+ class GatewayTimeout < ResponseError; end
133
+
130
134
  # HTTP status codes mapped to error classes.
131
135
  STATUS_MAPPINGS = {
132
136
  400 => BadRequest,
@@ -140,7 +144,8 @@ module Camper
140
144
  429 => TooManyRequests,
141
145
  500 => InternalServerError,
142
146
  502 => BadGateway,
143
- 503 => ServiceUnavailable
147
+ 503 => ServiceUnavailable,
148
+ 504 => GatewayTimeout
144
149
  }.freeze
145
150
  end
146
151
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Camper
4
- # Parses link header.
5
- #
6
- # @private
7
4
  class PaginationData
8
5
  include Logging
9
6
 
@@ -11,17 +11,26 @@ module Camper
11
11
  headers 'Accept' => 'application/json', 'Content-Type' => 'application/json'
12
12
  parser(proc { |body, _| parse(body) })
13
13
 
14
+ attr_reader :attempts
15
+
16
+ MAX_RETRY_ATTEMPTS = 5
17
+
14
18
  module Result
15
19
  ACCESS_TOKEN_EXPIRED = 'AccessTokenExpired'
16
20
 
21
+ TOO_MANY_REQUESTS = 'TooManyRequests'
22
+
17
23
  VALID = 'Valid'
18
24
  end
19
25
 
20
- def initialize(access_token, user_agent, client)
21
- @access_token = access_token
26
+ def initialize(client, method, path, options = {})
22
27
  @client = client
28
+ @path = path
29
+ @options = options
30
+ @attempts = 0
31
+ @method = method
23
32
 
24
- self.class.headers 'User-Agent' => user_agent
33
+ self.class.headers 'User-Agent' => @client.user_agent
25
34
  end
26
35
 
27
36
  # Converts the response body to a Resource.
@@ -50,32 +59,43 @@ module Camper
50
59
  raise Error::Parsing, 'The response is not a valid JSON'
51
60
  end
52
61
 
53
- %w[get post put delete].each do |method|
54
- define_method method do |path, options = {}|
55
- params = options.dup
56
- override_path = params.delete(:override_path)
62
+ # Executes the request
63
+ def execute
64
+ endpoint, params = prepare_request_data
57
65
 
58
- params[:headers] ||= {}
66
+ raise Error::TooManyRetries, endpoint if maxed_attempts?
59
67
 
60
- full_endpoint = override_path ? path : @client.api_endpoint + path
68
+ @attempts += 1
61
69
 
62
- execute_request(method, full_endpoint, params)
63
- end
70
+ logger.debug("Method: #{@method}; URL: #{endpoint}")
71
+
72
+ response, result = validate self.class.send(@method, endpoint, params)
73
+ response = extract_parsed(response) if result == Result::VALID
74
+
75
+ return response, result
76
+ end
77
+
78
+ def maxed_attempts?
79
+ @attempts >= MAX_RETRY_ATTEMPTS
64
80
  end
65
81
 
66
82
  private
67
83
 
68
- # Executes the request
69
- def execute_request(method, endpoint, params)
84
+ def prepare_request_data
85
+ params = @options.dup
86
+ override_path = params.delete(:override_path)
87
+
88
+ params[:body] = params[:body].to_json if body_to_json?(params)
89
+
90
+ params[:headers] ||= {}
70
91
  params[:headers].merge!(self.class.headers)
71
92
  params[:headers].merge!(authorization_header)
72
93
 
73
- logger.debug("Method: #{method}; URL: #{endpoint}")
74
- response, result = validate self.class.send(method, endpoint, params)
94
+ full_endpoint = override_path ? @path : @client.api_endpoint + @path
75
95
 
76
- response = extract_parsed(response) if result == Result::VALID
96
+ full_endpoint = url_transform(full_endpoint)
77
97
 
78
- return response, result
98
+ return full_endpoint, params
79
99
  end
80
100
 
81
101
  # Checks the response code for common errors.
@@ -90,9 +110,14 @@ module Camper
90
110
  return response, Result::ACCESS_TOKEN_EXPIRED
91
111
  end
92
112
 
113
+ if error_klass == Error::TooManyRequests
114
+ logger.debug('Too many request. Please check the Retry-After header for subsequent requests')
115
+ return response, Result::TOO_MANY_REQUESTS
116
+ end
117
+
93
118
  raise error_klass, response if error_klass
94
119
 
95
- return response, Result::Valid
120
+ return response, Result::VALID
96
121
  end
97
122
 
98
123
  def extract_parsed(response)
@@ -108,9 +133,24 @@ module Camper
108
133
  #
109
134
  # @raise [Error::MissingCredentials] if access_token and auth_token are not set.
110
135
  def authorization_header
111
- raise Error::MissingCredentials, 'Please provide a access_token' if @access_token.to_s.empty?
136
+ raise Error::MissingCredentials, 'Please provide a access_token' if @client.access_token.to_s.empty?
137
+
138
+ { 'Authorization' => "Bearer #{@client.access_token}" }
139
+ end
140
+
141
+ # Utility method for transforming Basecamp Web URLs into API URIs
142
+ # e.g 'https://3.basecamp.com/1/buckets/2/todos/3' will be
143
+ # converted into 'https://3.basecampapi.com/1/buckets/2/todos/3.json'
144
+ #
145
+ # @return [String]
146
+ def url_transform(url)
147
+ api_url = url.gsub('3.basecamp.com', '3.basecampapi.com')
148
+ api_url.gsub!('.json', '')
149
+ "#{api_url}.json"
150
+ end
112
151
 
113
- { 'Authorization' => "Bearer #{@access_token}" }
152
+ def body_to_json?(params)
153
+ @method == 'post' && params.key?(:body)
114
154
  end
115
155
  end
116
156
  end
@@ -63,11 +63,9 @@ module Camper
63
63
  @data.key?(method_name.to_s) ? @data[method_name.to_s] : super
64
64
  end
65
65
 
66
- # rubocop:disable Style/OptionalBooleanParameter
67
66
  def respond_to_missing?(method_name, include_private = false)
68
67
  @hash.keys.map(&:to_sym).include?(method_name.to_sym) || super
69
68
  end
70
- # rubocop:enable Style/OptionalBooleanParameter
71
69
 
72
70
  def self.detect_type(url)
73
71
  case url
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Camper
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.6'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: camper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - renehernandez
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-22 00:00:00.000000000 Z
11
+ date: 2020-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.9'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.9'
69
83
  description:
70
84
  email:
71
85
  executables: []
@@ -82,6 +96,7 @@ files:
82
96
  - ".rubocop.yml"
83
97
  - ".rubocop_todo.yml"
84
98
  - ".ruby-version"
99
+ - ".yardopts"
85
100
  - CHANGELOG.md
86
101
  - CONTRIBUTING.md
87
102
  - Gemfile
@@ -93,6 +108,7 @@ files:
93
108
  - bin/setup
94
109
  - camper.gemspec
95
110
  - examples/comments.rb
111
+ - examples/create_and_complete_todo.rb
96
112
  - examples/messages.rb
97
113
  - examples/oauth.rb
98
114
  - examples/obtain_acces_token.rb