procore 0.8.4 → 1.0.0

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: 7654871c3d9bcf462a38bae7694e54444da0ce52f53276b41baa26ae6440dfb2
4
- data.tar.gz: 68ba0f550d88869e4eebd382585df01a232b77ba5f0f84361b16da036a52f242
3
+ metadata.gz: ab6a771f9ecbd227e500591c59381e08e70b1ad3cea2f82e767b4de2d88664b3
4
+ data.tar.gz: 1159fa494f5d38ce2d0a0fbcbdb465bf6203e8989a96acf3bcceab915ea9dbac
5
5
  SHA512:
6
- metadata.gz: b52ad7bec7c5c7aa6d6e658a490a72d6f481c6f0c046aa247d58c703698d02170bb3db241ba801362d1bf8969f55e262ccb665ba5bb11d758c6fbccaaa4da429
7
- data.tar.gz: bf2b57f7595b6bf9fabdf9b75246700246e6722176d69502b447b2f264c331a326cef613ca10b215bce93a2d0d19821632480b5391bbb2298d8577daa9f10ce9
6
+ metadata.gz: e1c9f942d9c8223df00bff8c67b366adc85be3224d2a6ef41ee4e345d40cba8cc0ca71fe0361ff094be1ba358d3ad7ead40fc99d36bba8e5edac0dfc19932c57
7
+ data.tar.gz: afb9275ad4ec9b1396f3f7bed77317eb39b8e6cacf5d82dc77b38e9a934491e439795fe7df432d27fdc337800b7343327f1cb9d7b70c072ed72244bcd41d6a0a
@@ -1,5 +1,9 @@
1
+ require:
2
+ - rubocop-rails
3
+ - rubocop-performance
4
+
1
5
  AllCops:
2
- TargetRubyVersion: 2.3
6
+ TargetRubyVersion: 2.4
3
7
  Include:
4
8
  - '**/Rakefile'
5
9
  - '**/config.ru'
@@ -305,7 +309,7 @@ Style/PerlBackrefs:
305
309
  Naming/PredicateName:
306
310
  Description: 'Check the names of predicate methods.'
307
311
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark'
308
- NamePrefixBlacklist:
312
+ ForbiddenPrefixes:
309
313
  - is_
310
314
  Exclude:
311
315
  - spec/**/*
@@ -437,7 +441,7 @@ Style/RedundantBegin:
437
441
 
438
442
  # Layout
439
443
 
440
- Layout/AlignParameters:
444
+ Layout/ParameterAlignment:
441
445
  Description: 'Here we check if the parameters on a multi-line method call or definition are aligned.'
442
446
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent'
443
447
  Enabled: false
@@ -505,7 +509,7 @@ Lint/DeprecatedClassMethods:
505
509
  Description: 'Check for deprecated class method calls.'
506
510
  Enabled: false
507
511
 
508
- Lint/DuplicatedKey:
512
+ Lint/DuplicateHashKey:
509
513
  Description: 'Check for duplicate keys in hash literals.'
510
514
  Enabled: false
511
515
 
@@ -521,7 +525,7 @@ Lint/FormatParameterMismatch:
521
525
  Description: 'The number of parameters to format/sprint must match the fields.'
522
526
  Enabled: false
523
527
 
524
- Lint/HandleExceptions:
528
+ Lint/SuppressedException:
525
529
  Description: "Don't suppress exception."
526
530
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
527
531
  Enabled: false
@@ -3,9 +3,11 @@ rvm:
3
3
  - 2.3
4
4
  - 2.4
5
5
  - 2.5
6
+ - 2.6
6
7
 
7
8
  services:
8
9
  - redis-server
10
+ - memcached
9
11
 
10
12
  before_install:
11
13
  - gem update --system
@@ -1,4 +1,87 @@
1
- ## Unreleased
1
+ ## 1.0.0 (January 5, 2021)
2
+
3
+ * Adds support for API versioning
4
+
5
+ *Nate Baer*
6
+
7
+ ### Upgrading
8
+
9
+ As of v1.0.0, this gem now defaults to making requests against Procore's new
10
+ Rest v1.0 resources, instead of the now deprecated `/vapid` namespace. Example:
11
+
12
+ ```ruby
13
+ # Previously makes a request to
14
+ client.get("me")
15
+ => app.procore.com/vapid/me
16
+
17
+ # In 1.0.0
18
+ client.get("me")
19
+ => app.procore.com/rest/v1.0/me
20
+ ```
21
+
22
+ To keep the legacy behavior, set the new `default_version` configuration option.
23
+ Note, that Rest v1.0 is a superset of the Vapid Api - there are no breaking
24
+ changes. The Vapid API will be decommissioned in December 2021.
25
+
26
+ [Read more here](https://developers.procore.com/documentation/vapid-deprecation)
27
+
28
+ ```ruby
29
+ Procore.configure do |config|
30
+ ...
31
+ # Defaults to "v1.0"
32
+ config.default_version = "vapid"
33
+ ...
34
+ end
35
+ ```
36
+
37
+ All the request methods (`get`, `post`, `patch`, `put`, `delete`, `sync`) now
38
+ accept an optional version parameter to specify the version at request time.
39
+
40
+ ```ruby
41
+ client.get("me")
42
+ => https://app.procore.com/rest/v1.0/me
43
+
44
+ client.get("me", version: "v1.1")
45
+ => https://app.procore.com/rest/v1.1/me
46
+
47
+ client.get("me", version: "vapid")
48
+ => https://app.procore.com/vapid/me
49
+ ```
50
+
51
+ ## 0.8.8 (October 17, 2019)
52
+
53
+ * Expose #sync, a method that enables calling sync-actions
54
+
55
+ *Patrick Koperwas*
56
+
57
+ * Addition of contribution guidelines to README
58
+
59
+ *Megan O'Neill*
60
+
61
+ * Fix TravisCI failures
62
+
63
+ *Patrick Koperwas*
64
+
65
+ ## 0.8.7 (April 18, 2019)
66
+
67
+ * Add api_version to allow calls to procore rest endpoints
68
+
69
+ *Shane Means*
70
+
71
+ ## 0.8.6 (May 10, 2018)
72
+
73
+ * Dalli Store
74
+
75
+ *Patrick Koperwas*
76
+
77
+ * Fix Requestable paths to prevent double slash in URI
78
+
79
+ *Megan O'Neill*
80
+
81
+ ## 0.8.5 (May 9, 2018)
82
+ * Rescue Errno::ECONNREFUSED errors and RestClient::ServerBrokeConnection
83
+
84
+ *Casey Ochs*
2
85
 
3
86
  ## 0.8.4 (May 8, 2018)
4
87
 
data/README.md CHANGED
@@ -4,20 +4,24 @@
4
4
 
5
5
  #### Table of Contents
6
6
  - [Installation](#installation)
7
+ - [1.0.0 Release](#1.0.0-release)
7
8
  - [Making Requests](#making-requests)
8
9
  - [Usage](#usage)
9
10
  - [Error Handling](#error-handling)
10
11
  - [Pagination](#pagination)
11
12
  - [Navigating Through Paginated Results](#navigating-through-paginated-results)
12
13
  - [Change Number of Results](#change-number-of-results)
14
+ - [Sync Actions](#sync-actions)
13
15
  - [Configuration](#configuration)
14
16
  - [Stores](#stores)
15
17
  - [Session Store](#session-store)
16
18
  - [Redis Store](#redis-store)
19
+ - [Dalli Store](#dalli-store)
17
20
  - [ActiveRecord Store](#activerecord-store)
18
21
  - [File Store](#file-store)
19
22
  - [Memory Store](#memory-store)
20
23
  - [Full Example](#full-example)
24
+ - [Contributing](#contributing)
21
25
 
22
26
  ## Installation
23
27
 
@@ -27,6 +31,11 @@ Add this line to your application's Gemfile:
27
31
  gem "procore"
28
32
  ```
29
33
 
34
+ ## 1.0.0 Release
35
+
36
+ v1.0.0 was released on January 5, 2021, and adds support the new Rest v1.0 API.
37
+ See the CHANGELOG for upgrade instructions.
38
+
30
39
  ## Making Requests
31
40
 
32
41
  At the core of the gem is the `Client` class. Clients are initialized with a
@@ -38,19 +47,27 @@ Stores automatically manage tokens for you - refreshing, revoking and storage
38
47
  are abstracted away to make your code as simple as possible. There are several
39
48
  different [types of stores](#stores) available to you.
40
49
 
41
- The Client class exposes `#get`, `#post`, `#put`, `#patch` and `#delete` methods
42
- to you.
50
+ The Client class exposes `#get`, `#post`, `#put`, `#patch`, `#sync` and
51
+ `#delete` methods to you.
43
52
 
44
53
  ```ruby
45
- get(path, query: {})
46
- post(path, body: {}, options: {})
47
- put(path, body: {}, options: {})
48
- patch(path, body: {}, options: {})
49
- delete(path, query: {})
54
+ get(path, version: "", query: {})
55
+ post(path, version: "", body: {}, options: {})
56
+ put(path, version: "", body: {}, options: {})
57
+ patch(path, version: "", body: {}, options: {})
58
+ delete(path, version: "", query: {})
59
+ sync(path, version: "", body: {}, options: {})
50
60
  ```
51
61
 
52
- All paths are relative - the gem will handle expanding `client.get("me")` to
53
- `https://app.procore.com/vapid/me`.
62
+ All paths are relative, the gem will handle expanding them. An API version may
63
+ be specified in the `version:` argument, or the default version is used. The
64
+ default version is `v1.0` unless otherwise configured.
65
+
66
+ | Example | Requested URL |
67
+ | --- | --- |
68
+ | `client.get("me")` | `https://app.procore.com/rest/v1.0/me` |
69
+ | `client.get("me", version: "v1.1")` | `https://app.procore.com/rest/v1.1/me` |
70
+ | `client.get("me", version: "vapid")` | `https://app.procore.com/vapid/me` |
54
71
 
55
72
  Example Usage:
56
73
 
@@ -68,6 +85,19 @@ companies = client.get("companies")
68
85
  companies.first[:name] #=> "Procore Company 1"
69
86
  ```
70
87
 
88
+ To use Procore's older API Vapid by default, the default version can be set in
89
+ either the Gem's [configuration](https://github.com/procore/ruby-sdk#configuration)
90
+ or the client's `options` hash:
91
+
92
+ ```ruby
93
+ client = Procore::Client.new(
94
+ ...
95
+ options {
96
+ default_version: "vapid"
97
+ }
98
+ )
99
+ ```
100
+
71
101
  ## Usage
72
102
 
73
103
  The first step is to place the user's token into the store. For this example,
@@ -258,6 +288,49 @@ puts first_page.pagination
258
288
  Notice that because `per_page` has been set to 250, there are only two pages of
259
289
  results (500 resources / 250 page size = 2 pages).
260
290
 
291
+ ## Sync Actions
292
+ The Sync action enables batch creation or updates to resources using a single
293
+ call. When using a Sync action, the resources to be created or updated can be
294
+ specified by supplying either an `id` or an `origin_id` in the request body.
295
+ Utilizing the `origin_id` attribute for batch operations is often preferable as
296
+ it allows you to easily link to external systems by maintaining your own list of
297
+ unique resource identifiers outside of Procore.
298
+
299
+ The caller provides an array of hashes, each hash containing the attributes for
300
+ a single resource. The attribute names in each hash match those used by the
301
+ Create and Update actions for the resource. Attributes for a maximum of 1000
302
+ resources within a collection may be passed with each call. The API will always
303
+ return an HTTP status of 200.
304
+
305
+ The response body contains two attributes - `entities` and `errors`. The
306
+ attributes for each successfully created or updated resource will appear in the
307
+ entities list. The attributes for each resource will match those returned by the
308
+ Show action. For each resource which could not be created or updated, the
309
+ attributes supplied by the caller are present in the errors list, along with an
310
+ additional errors attribute which provides reasons for the failure.
311
+
312
+ [Continue reading
313
+ here.](https://developers.procore.com/documentation/using-sync-actions)
314
+
315
+ Example Usage:
316
+
317
+ ```ruby
318
+ client.sync(
319
+ "projects/sync",
320
+ body: {
321
+ updates: [
322
+ { id: 1, name: "Update 1" },
323
+ { id: 2, name: "Update 2" },
324
+ { id: 3, name: "Update 3" },
325
+ ...
326
+ ...
327
+ { id: 5055, name: "Update 5055" },
328
+ ]
329
+ },
330
+ options: { batch_size: 500, company_id: 1 },
331
+ )
332
+ ```
333
+
261
334
  ## Configuration
262
335
 
263
336
  The Procore Gem exposes a configuration with several options.
@@ -272,6 +345,15 @@ Procore.configure do |config|
272
345
  # instead of production.
273
346
  config.host = ENV.fetch("PROCORE_BASE_API_PATH", "https://app.procore.com")
274
347
 
348
+ # When using #sync action, sets the default batch size to use for chunking
349
+ # up a request body. Example: if the size is set to 500, and 2,000 updates
350
+ # are desired, 4 requests will be made. Note, the maximum size is 1000.
351
+ config.default_batch_size = 500
352
+
353
+ # The default API version to use if none is specified in the request.
354
+ # Should be either "v1.0" (recommended) or "vapid" (legacy).
355
+ config.default_version = "v1.0"
356
+
275
357
  # Integer: Number of times to retry a failed API call. Reasons an API call
276
358
  # could potentially fail:
277
359
  # 1. Service is briefly down or unreachable
@@ -313,6 +395,14 @@ Options: `session`: Instance of a Rails session
313
395
 
314
396
  For applications that want to keep access tokens in the user's session.
315
397
 
398
+ :warning:
399
+ We strongly discourage using the session as a token store since the rails
400
+ session is often logged by default to external apps such as bugsnag etc. Be sure
401
+ you are not logging tokens. There is also the possibility that the rails session
402
+ is using a cookie store which, depending on application settings, could be
403
+ unencrypted. Tokens should not be stored client-side if it can be avoided.
404
+ :warning:
405
+
316
406
  ```ruby
317
407
  store = Procore::Auth::Stores::Session.new(session: session)
318
408
  ```
@@ -331,6 +421,20 @@ The key will usually be the id of the current user.
331
421
  store = Procore::Auth::Stores::Redis.new(redis: Redis.new, key: current_user.id)
332
422
  ```
333
423
 
424
+ ### Dalli Store
425
+
426
+ Options: `dalli`: Instance of Dalli
427
+ Options: `key`: Unique identifier to an access token
428
+
429
+ For applications which want to store access tokens in memcached using Dalli.
430
+ There's two required options, `dalli` which is an instance of a Dalli client,
431
+ and `key` which is a unique key which will be used to save / retrieve an access
432
+ token. The key will usually be the id of the current user.
433
+
434
+ ```ruby
435
+ store = Procore::Auth::Stores::Dalli.new(dalli: Dalli.new, key: current_user.id)
436
+ ```
437
+
334
438
  ### ActiveRecord Store
335
439
 
336
440
  Options: `object`: Instance of an ActiveRecord model.
@@ -410,6 +514,27 @@ class ProjectsController
410
514
  end
411
515
  end
412
516
  ```
517
+ ## Contributing
518
+
519
+ To contribute to the gem, please clone the repo and cut a new branch. In the PR update the changelog with a short explanation of what you've changed, and your name under the "Unreleased" section. Example changelog update:
520
+
521
+ ```markdown
522
+ ## Unreleased
523
+
524
+ * Short sentence of what has changed
525
+
526
+ *Your Name*
527
+ ```
528
+
529
+ Please **do not** bump the gem version in your PR. This will be done in a follow up PR by the gem maintainers.
530
+
531
+ ### Tests
532
+
533
+ To run the specs run the following command:
534
+ ```bash
535
+ $ bundle exec rake test
536
+ ```
537
+
413
538
 
414
539
  ## License
415
540
 
@@ -7,6 +7,7 @@ require "json"
7
7
  require "procore/auth/access_token_credentials"
8
8
  require "procore/auth/client_credentials"
9
9
  require "procore/auth/stores/active_record"
10
+ require "procore/auth/stores/dalli"
10
11
  require "procore/auth/stores/file"
11
12
  require "procore/auth/stores/memory"
12
13
  require "procore/auth/stores/redis"
@@ -29,7 +29,7 @@ module Procore
29
29
  raise OAuthError.new(e.description, response: e.response)
30
30
  rescue Faraday::ConnectionFailed => e
31
31
  raise APIConnectionError.new("Connection Error: #{e.message}")
32
- rescue URI::BadURIError
32
+ rescue URI::BadURIError, URI::InvalidURIError
33
33
  raise OAuthError.new(
34
34
  "Host is not a valid URI. Check your host option to make sure it " \
35
35
  "is a properly formed url",
@@ -0,0 +1,38 @@
1
+ module Procore
2
+ module Auth
3
+ module Stores
4
+ class Dalli
5
+ attr_reader :key, :dalli
6
+ def initialize(key:, dalli:)
7
+ @key = key
8
+ @dalli = dalli
9
+ end
10
+
11
+ def save(token)
12
+ dalli.set(dalli_key, token.to_json)
13
+ end
14
+
15
+ def fetch
16
+ return unless dalli.get(dalli_key)
17
+
18
+ token = JSON.parse(dalli.get(dalli_key))
19
+ Procore::Auth::Token.new(
20
+ access_token: token["access_token"],
21
+ refresh_token: token["refresh_token"],
22
+ expires_at: token["expires_at"],
23
+ )
24
+ end
25
+
26
+ def delete
27
+ dalli.delete(dalli_key)
28
+ end
29
+
30
+ private
31
+
32
+ def dalli_key
33
+ "procore-dalli-#{key}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -41,7 +41,7 @@ module Procore
41
41
  private
42
42
 
43
43
  def base_api_path
44
- "#{options[:host]}/vapid"
44
+ "#{options[:host]}"
45
45
  end
46
46
 
47
47
  # @raise [OAuthError] if the store does not have a token stored in it prior
@@ -32,6 +32,15 @@ module Procore
32
32
  # @return [String]
33
33
  attr_accessor :host
34
34
 
35
+ # @!attribute [rw] default_version
36
+ # @note defaults to Defaults::DEFAULT_VERSION
37
+ #
38
+ # The default API version to use if none is specified in the request.
39
+ # Should be either "v1.0" (recommended) or "vapid" (legacy).
40
+ #
41
+ # @return [String]
42
+ attr_accessor :default_version
43
+
35
44
  # @!attribute [rw] logger
36
45
  # @note defaults to nil
37
46
  #
@@ -77,12 +86,26 @@ module Procore
77
86
  # @return [String]
78
87
  attr_accessor :user_agent
79
88
 
89
+ # @!attribute [rw] default_batch_size
90
+ # @note defaults to Defaults::BATCH_SIZE
91
+ #
92
+ # When using #sync action, sets the default batch size to use for chunking
93
+ # up a request body. Example, if the size is set to 500 and 2,000 updates
94
+ # are desired, 4 requests will be made.
95
+ #
96
+ # Note: The maximum size is 1,000
97
+ #
98
+ # @return [Integer]
99
+ attr_accessor :default_batch_size
100
+
80
101
  def initialize
102
+ @default_batch_size = Procore::Defaults::BATCH_SIZE
81
103
  @host = Procore::Defaults::API_ENDPOINT
82
104
  @logger = nil
83
105
  @max_retries = 1
84
106
  @timeout = 1.0
85
107
  @user_agent = Procore::Defaults::USER_AGENT
108
+ @default_version = Procore::Defaults::DEFAULT_VERSION
86
109
  end
87
110
  end
88
111
  end
@@ -9,10 +9,17 @@ module Procore
9
9
  # Default User Agent header string
10
10
  USER_AGENT = "Procore Ruby Gem #{Procore::VERSION}".freeze
11
11
 
12
+ # Default size to use for batch requests
13
+ BATCH_SIZE = 500
14
+
15
+ # Default API version to use
16
+ DEFAULT_VERSION = "v1.0"
17
+
12
18
  def self.client_options
13
19
  {
14
20
  host: Procore.configuration.host,
15
21
  user_agent: Procore.configuration.user_agent,
22
+ default_version: Procore.configuration.default_version,
16
23
  }
17
24
  end
18
25
  end
@@ -1,4 +1,5 @@
1
1
  require "rest-client"
2
+ require "procore/errors"
2
3
 
3
4
  module Procore
4
5
  # Module which defines HTTP verbs GET, POST, PUT, PATCH and DELETE. Is
@@ -10,7 +11,15 @@ module Procore
10
11
  # @example Using #post:
11
12
  # client.post("projects", name: "New Project")
12
13
  module Requestable
14
+ HTTP_EXCEPTIONS = [
15
+ Errno::ECONNREFUSED,
16
+ Errno::ECONNRESET,
17
+ Procore::OAuthError,
18
+ RestClient::Exceptions::Timeout,
19
+ RestClient::ServerBrokeConnection,
20
+ ].freeze
13
21
  # @param path [String] URL path
22
+ # @param version [String] API version
14
23
  # @param query [Hash] Query options to pass along with the request
15
24
  # @option options [Hash] :company_id
16
25
  #
@@ -18,10 +27,12 @@ module Procore
18
27
  # client.get("my_open_items", query: { per_page: 5, filter: {} })
19
28
  #
20
29
  # @return [Response]
21
- def get(path, query: {}, options: {})
30
+ def get(path, version: nil, query: {}, options: {})
31
+ full_path = full_path(path, version)
32
+
22
33
  Util.log_info(
23
34
  "API Request Initiated",
24
- path: "#{base_api_path}/#{path}",
35
+ path: full_path,
25
36
  method: "GET",
26
37
  query: query.to_s,
27
38
  )
@@ -29,7 +40,7 @@ module Procore
29
40
  with_response_handling do
30
41
  RestClient::Request.execute(
31
42
  method: :get,
32
- url: "#{base_api_path}/#{path}",
43
+ url: full_path,
33
44
  headers: headers(options).merge(params: query),
34
45
  timeout: Procore.configuration.timeout,
35
46
  )
@@ -37,8 +48,9 @@ module Procore
37
48
  end
38
49
 
39
50
  # @param path [String] URL path
51
+ # @param version [String] API version
40
52
  # @param body [Hash] Body parameters to send with the request
41
- # @param options [Hash} Extra request options
53
+ # @param options [Hash] Extra request options
42
54
  # @option options [String] :idempotency_token | :company_id
43
55
  #
44
56
  # @example Usage
@@ -49,10 +61,12 @@ module Procore
49
61
  # )
50
62
  #
51
63
  # @return [Response]
52
- def post(path, body: {}, options: {})
64
+ def post(path, version: nil, body: {}, options: {})
65
+ full_path = full_path(path, version)
66
+
53
67
  Util.log_info(
54
68
  "API Request Initiated",
55
- path: "#{base_api_path}/#{path}",
69
+ path: full_path,
56
70
  method: "POST",
57
71
  body: body.to_s,
58
72
  )
@@ -60,7 +74,7 @@ module Procore
60
74
  with_response_handling(request_body: body) do
61
75
  RestClient::Request.execute(
62
76
  method: :post,
63
- url: "#{base_api_path}/#{path}",
77
+ url: full_path,
64
78
  payload: payload(body),
65
79
  headers: headers(options),
66
80
  timeout: Procore.configuration.timeout,
@@ -69,18 +83,21 @@ module Procore
69
83
  end
70
84
 
71
85
  # @param path [String] URL path
86
+ # @param version [String] API version
72
87
  # @param body [Hash] Body parameters to send with the request
73
- # @param options [Hash} Extra request options
88
+ # @param options [Hash] Extra request options
74
89
  # @option options [String] :idempotency_token | :company_id
75
90
  #
76
91
  # @example Usage
77
92
  # client.put("dashboards/1/users", body: [1,2,3], options: { company_id: 1 })
78
93
  #
79
94
  # @return [Response]
80
- def put(path, body: {}, options: {})
95
+ def put(path, version: nil, body: {}, options: {})
96
+ full_path = full_path(path, version)
97
+
81
98
  Util.log_info(
82
99
  "API Request Initiated",
83
- path: "#{base_api_path}/#{path}",
100
+ path: full_path,
84
101
  method: "PUT",
85
102
  body: body.to_s,
86
103
  )
@@ -88,7 +105,7 @@ module Procore
88
105
  with_response_handling(request_body: body) do
89
106
  RestClient::Request.execute(
90
107
  method: :put,
91
- url: "#{base_api_path}/#{path}",
108
+ url: full_path,
92
109
  payload: payload(body),
93
110
  headers: headers(options),
94
111
  timeout: Procore.configuration.timeout,
@@ -97,8 +114,9 @@ module Procore
97
114
  end
98
115
 
99
116
  # @param path [String] URL path
117
+ # @param version [String] API version
100
118
  # @param body [Hash] Body parameters to send with the request
101
- # @param options [Hash} Extra request options
119
+ # @param options [Hash] Extra request options
102
120
  # @option options [String] :idempotency_token | :company_id
103
121
  #
104
122
  # @example Usage
@@ -109,10 +127,12 @@ module Procore
109
127
  # )
110
128
  #
111
129
  # @return [Response]
112
- def patch(path, body: {}, options: {})
130
+ def patch(path, version: nil, body: {}, options: {})
131
+ full_path = full_path(path, version)
132
+
113
133
  Util.log_info(
114
134
  "API Request Initiated",
115
- path: "#{base_api_path}/#{path}",
135
+ path: full_path,
116
136
  method: "PATCH",
117
137
  body: body.to_s,
118
138
  )
@@ -120,7 +140,7 @@ module Procore
120
140
  with_response_handling(request_body: body) do
121
141
  RestClient::Request.execute(
122
142
  method: :patch,
123
- url: "#{base_api_path}/#{path}",
143
+ url: full_path,
124
144
  payload: payload(body),
125
145
  headers: headers(options),
126
146
  timeout: Procore.configuration.timeout,
@@ -129,6 +149,73 @@ module Procore
129
149
  end
130
150
 
131
151
  # @param path [String] URL path
152
+ # @param version [String] API version
153
+ # @param body [Hash] Body parameters to send with the request
154
+ # @param options [Hash] Extra request options
155
+ # @option options [String | Integer] :company_id | :batch_size
156
+ #
157
+ # @example Usage
158
+ # client.sync(
159
+ # "projects/sync",
160
+ # body: {
161
+ # updates: [
162
+ # { id: 1, name: "Update 1" },
163
+ # { id: 2, name: "Update 2" },
164
+ # { id: 3, name: "Update 3" },
165
+ # ...
166
+ # ...
167
+ # { id: 5055, name: "Update 5055" },
168
+ # ]
169
+ # },
170
+ # options: { batch_size: 500, company_id: 1 },
171
+ # )
172
+ #
173
+ # @return [Response]
174
+ def sync(path, version: nil, body: {}, options: {})
175
+ full_path = full_path(path, version)
176
+
177
+ batch_size = options[:batch_size] ||
178
+ Procore.configuration.default_batch_size
179
+
180
+ if batch_size > 1000
181
+ batch_size = 1000
182
+ end
183
+
184
+ Util.log_info(
185
+ "API Request Initiated",
186
+ path: full_path,
187
+ method: "SYNC",
188
+ batch_size: batch_size,
189
+ )
190
+
191
+ groups = body[:updates].each_slice(batch_size).to_a
192
+
193
+ responses = groups.map do |group|
194
+ batched_body = body.merge(updates: group)
195
+ with_response_handling(request_body: batched_body) do
196
+ RestClient::Request.execute(
197
+ method: :patch,
198
+ url: full_path,
199
+ payload: payload(batched_body),
200
+ headers: headers(options),
201
+ timeout: Procore.configuration.timeout,
202
+ )
203
+ end
204
+ end
205
+
206
+ Procore::Response.new(
207
+ body: responses.reduce({}) do |combined, response|
208
+ combined.deep_merge(response.body) { |_, v1, v2| v1 + v2 }
209
+ end.to_json,
210
+ headers: responses.map(&:headers).inject({}, &:deep_merge),
211
+ code: 200,
212
+ request: responses.last&.request,
213
+ request_body: body,
214
+ )
215
+ end
216
+
217
+ # @param path [String] URL path
218
+ # @param version [String] API version
132
219
  # @param query [Hash] Query options to pass along with the request
133
220
  # @option options [String] :company_id
134
221
  #
@@ -136,10 +223,12 @@ module Procore
136
223
  # client.delete("users/1", query: {}, options: {})
137
224
  #
138
225
  # @return [Response]
139
- def delete(path, query: {}, options: {})
226
+ def delete(path, version: nil, query: {}, options: {})
227
+ full_path = full_path(path, version)
228
+
140
229
  Util.log_info(
141
230
  "API Request Initiated",
142
- path: "#{base_api_path}/#{path}",
231
+ path: full_path,
143
232
  method: "DELETE",
144
233
  headers: headers(options),
145
234
  query: query.to_s,
@@ -148,7 +237,7 @@ module Procore
148
237
  with_response_handling do
149
238
  RestClient::Request.execute(
150
239
  method: :delete,
151
- url: "#{base_api_path}/#{path}",
240
+ url: full_path,
152
241
  headers: headers.merge(params: query),
153
242
  timeout: Procore.configuration.timeout,
154
243
  )
@@ -163,7 +252,7 @@ module Procore
163
252
 
164
253
  begin
165
254
  result = yield
166
- rescue RestClient::Exceptions::Timeout, Errno::ECONNREFUSED, Procore::OAuthError => e
255
+ rescue *HTTP_EXCEPTIONS => e
167
256
  if retries <= Procore.configuration.max_retries
168
257
  retries += 1
169
258
  sleep 1.5**retries
@@ -277,5 +366,16 @@ module Procore
277
366
  def multipart?(body)
278
367
  RestClient::Payload::has_file?(body)
279
368
  end
369
+
370
+ def full_path(path, version)
371
+ version ||= options[:default_version]
372
+ if version == "vapid"
373
+ File.join(base_api_path, "vapid", path)
374
+ elsif /\Av\d+\.\d+\z/.match?(version)
375
+ File.join(base_api_path, "rest", version, path)
376
+ else
377
+ raise ArgumentError.new "'#{version}' is an invalid Procore API version"
378
+ end
379
+ end
280
380
  end
281
381
  end
@@ -72,7 +72,7 @@ module Procore
72
72
 
73
73
  def parse_pagination
74
74
  headers[:link].to_s.split(", ").map(&:strip).reduce({}) do |links, link|
75
- url, name = link.match(/vapid\/(.*?)>; rel="(\w+)"/).captures
75
+ url, name = link.match(/(?:vapid|rest\/.*?)\/(.*?)>; rel="(\w+)"/).captures
76
76
  links.merge!(name.to_sym => url)
77
77
  end
78
78
  end
@@ -1,3 +1,3 @@
1
1
  module Procore
2
- VERSION = "0.8.4".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -25,12 +25,15 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency "actionpack"
26
26
  spec.add_development_dependency "activerecord"
27
27
  spec.add_development_dependency "bundler"
28
+ spec.add_development_dependency "dalli"
28
29
  spec.add_development_dependency "fakefs"
29
30
  spec.add_development_dependency "minitest"
30
31
  spec.add_development_dependency "pry"
31
32
  spec.add_development_dependency "rake"
32
33
  spec.add_development_dependency "redis"
33
34
  spec.add_development_dependency "rubocop"
35
+ spec.add_development_dependency "rubocop-performance"
36
+ spec.add_development_dependency "rubocop-rails"
34
37
  spec.add_development_dependency "sqlite3"
35
38
  spec.add_development_dependency "webmock"
36
39
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Procore Engineering
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-08 00:00:00.000000000 Z
11
+ date: 2021-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dalli
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: fakefs
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -136,6 +150,34 @@ dependencies:
136
150
  - - ">="
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-performance
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop-rails
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
139
181
  - !ruby/object:Gem::Dependency
140
182
  name: sqlite3
141
183
  requirement: !ruby/object:Gem::Requirement
@@ -227,6 +269,7 @@ files:
227
269
  - lib/procore/auth/access_token_credentials.rb
228
270
  - lib/procore/auth/client_credentials.rb
229
271
  - lib/procore/auth/stores/active_record.rb
272
+ - lib/procore/auth/stores/dalli.rb
230
273
  - lib/procore/auth/stores/file.rb
231
274
  - lib/procore/auth/stores/memory.rb
232
275
  - lib/procore/auth/stores/redis.rb
@@ -260,8 +303,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
260
303
  - !ruby/object:Gem::Version
261
304
  version: '0'
262
305
  requirements: []
263
- rubyforge_project:
264
- rubygems_version: 2.7.6
306
+ rubygems_version: 3.1.4
265
307
  signing_key:
266
308
  specification_version: 4
267
309
  summary: Procore Ruby Gem