camper 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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