procore 0.8.6 → 1.1.1

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
- SHA256:
3
- metadata.gz: 6545e8ea432e0c10b0a3cd0e1257e96b57bf18f44c6d3ecfd3fa5805212290db
4
- data.tar.gz: c93910a023d455bcc2731f8f74a1ebe3631851c0b711cf4effff9f1fe84b4503
2
+ SHA1:
3
+ metadata.gz: d521ee6322fa923e85f77d6c836d7e5f9cb208df
4
+ data.tar.gz: 48eee44692be1ce01bd486ec57dc05699b1c9198
5
5
  SHA512:
6
- metadata.gz: 6376d8a7cc0a0a494594ce185e77630b885da38bbd14614219f069e3eaccd7f0f7cc202f007ef58129db49910bd518523e285830fbd3d6c379d920fdeceef6f9
7
- data.tar.gz: e258551108c844d80137919ce861233db506ba97d72598f93da3c2e7818425188857a42be0ef2b3d28dde99e95eb304f5d11b179b2f470c45dfde37574e7b742
6
+ metadata.gz: a94c2bd6877729616f381027130ee9216093a9fc5ffb9bc2af473dffa45ac1050d39a280fd92ad6e6978ff6b8d2551afe464002743a46d0b6b34be9a5161a86d
7
+ data.tar.gz: aaa762a18d06ea9e432111fadd89ae6eb389ff645ef2f9b488278f1710c3d368595054565e5d7b97ece4f9e73f6c6c3ff80d00c628658613c47f712b82261d42
data/.rubocop.yml CHANGED
@@ -1,5 +1,9 @@
1
+ require:
2
+ - rubocop-rails
3
+ - rubocop-performance
4
+
1
5
  AllCops:
2
- TargetRubyVersion: 2.3
6
+ TargetRubyVersion: 2.5
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
data/.travis.yml CHANGED
@@ -1,28 +1,25 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.3
4
- - 2.4
5
- - 2.5
6
-
3
+ - 2.3
4
+ - 2.4
5
+ - 2.5
6
+ - 2.6
7
7
  services:
8
- - redis-server
9
-
8
+ - redis-server
9
+ - memcached
10
10
  before_install:
11
- - gem update --system
12
- - gem install bundler
13
-
11
+ - gem update --system
12
+ - gem install bundler
14
13
  cache: bundler
15
-
16
14
  script: bundle exec rake
17
-
18
15
  deploy:
19
16
  provider: rubygems
20
17
  api_key:
21
- secure: GYH7nTi2D9ZwbCP+JYSgHceDUvZT9RQSP6yXFndwSyxdiSmB6wfq6EtoH4Wpznd6ALT6IxrtbR4aftTI9DO6HPq+PVO6NdxjkdlbdxxAqRQJBAwFwFATT8jCuuDjGPLf8PF0ouzcMvAIdIgHypwmLdevWdEMomWdRWlm1UFP/0C3RDjY1v04XDrRb+fEagPNnzMDCcO352nyFm06jnCq/ezjCRXBvNtHTZeqd7t9pbNw7gtYebZ3b9hfd4IIa68budeh+URygiPnNNWr6TCjO95IuHIeMDele3tir0KzqVBNsVAqEb7WpB26rf1JgneAoYZwVMFh+hk09cs68EbIELD4MX6/PzEUz2Ws20dIMjs1pcDtlw35ixoEpwC/vz2qVJyMRYXg8af5rpsZkKXRl7QP2LrX+xSEt4LiRnylj4PaRZWu1gvhC7fsCHXEM3mW3WlXFUmb9W4Jv8VooqOJtGhpD9Qs9pYtTOgJoEmYqttbuCaiYKaiPutBY4+t+t2ZmyCopv41nkjv/UCWXfo11ew1qK0rxDJVE2/JYTUDZ+zuHypTf2lHCWjyP65vGwBnmdixLnEMCUOkzKUmqFR4kAE6jRMEhtki4fTlFSIHJGM5oIXeRXDR43NIh7xWNvhl7lHkwWoRA0BfSITtvO8k0SefGnFwhNfOglQ/GKfyAaY=
22
- gem: procore
18
+ secure: LC+5qDeaqBtA8IIDt0yHNYsTA/KEs7IK/hESF2V2f4bhmMapifEVSjikoZVE1lTe0R2+CyO+5mWHloFeTLayTchpZhW3yfduei3l2tktZOH9UCsXIQZKveSZlCMQ+XOn65Gstbd/5krh9+axKZswgM5YgBhjXNRgjL+yOGh2kXN0exk5nBvIBFxSCQdwqGiBDcrtgJ4eWrlPgofXszMFb3ogNQcIuUcKmAOC0j/fNag4qQV0jnp3zHDuOjrsARPJn4w6RMqSLpFVUYpJw3tE94GDW5kILHr7taTPj12PeYNbYjW7kRSgMYbQlIWUKLeyMp+vw3kNqd3Ysat+M+Mrf3z2KRKR5w62IJnCq758uerCdUo5v/vsWHYFcJvZWLrvsPSk+xQWysAqRdkun7ZBCK/FLR75S6Vkm0DBnbqd/yYp6MP9Vbi77gIZpWMXANfdk1aWGVlPNpskYeWyqPjwZX1nYybJQIIpZfX21F2WPrNA9r/cj1KYjb8vKopMEdn9GP9xMeUXXu85K+ZVPqSD/AblrLHyD2x7cES/i4+XcQSaKUOSLxTUqemAiDRBahDCehPOOeLHnu/rUzgQr1gzQL0YcLNgHovmIdstljBys62JC4gy376cNawiSW4ZPMHWg6PewqQ6Ozg4Lkytx06bBwOos55dFgI4k0T4H0/6QiE=
19
+ gem: ruby-sdk
23
20
  on:
24
21
  tags: true
25
22
  repo: procore/ruby-sdk
26
-
23
+ skip_cleanup: 'true'
27
24
  notifications:
28
25
  email: false
data/CHANGELOG.md CHANGED
@@ -1,4 +1,88 @@
1
- ## Unreleased
1
+ ## 1.1.1 (May 5, 2021)
2
+
3
+ * Change default host to 'api.procore.com'.
4
+
5
+ PR #42 - https://github.com/procore/ruby-sdk/pull/42
6
+
7
+ *Nate Baer*
8
+
9
+ ## 1.1.0 (March 11, 2021)
10
+
11
+ * Allow tokens to be revoked and manually refreshed.
12
+
13
+ PR #39 - https://github.com/procore/ruby-sdk/pull/39
14
+
15
+ *Nate Baer*
16
+
17
+ ## 1.0.0 (January 5, 2021)
18
+
19
+ * Adds support for API versioning
20
+
21
+ *Nate Baer*
22
+
23
+ ### Upgrading
24
+
25
+ As of v1.0.0, this gem now defaults to making requests against Procore's new
26
+ Rest v1.0 resources, instead of the now deprecated `/vapid` namespace. Example:
27
+
28
+ ```ruby
29
+ # Previously makes a request to
30
+ client.get("me")
31
+ => app.procore.com/vapid/me
32
+
33
+ # In 1.0.0
34
+ client.get("me")
35
+ => app.procore.com/rest/v1.0/me
36
+ ```
37
+
38
+ To keep the legacy behavior, set the new `default_version` configuration option.
39
+ Note, that Rest v1.0 is a superset of the Vapid Api - there are no breaking
40
+ changes. The Vapid API will be decommissioned in December 2021.
41
+
42
+ [Read more here](https://developers.procore.com/documentation/vapid-deprecation)
43
+
44
+ ```ruby
45
+ Procore.configure do |config|
46
+ ...
47
+ # Defaults to "v1.0"
48
+ config.default_version = "vapid"
49
+ ...
50
+ end
51
+ ```
52
+
53
+ All the request methods (`get`, `post`, `patch`, `put`, `delete`, `sync`) now
54
+ accept an optional version parameter to specify the version at request time.
55
+
56
+ ```ruby
57
+ client.get("me")
58
+ => https://app.procore.com/rest/v1.0/me
59
+
60
+ client.get("me", version: "v1.1")
61
+ => https://app.procore.com/rest/v1.1/me
62
+
63
+ client.get("me", version: "vapid")
64
+ => https://app.procore.com/vapid/me
65
+ ```
66
+
67
+ ## 0.8.8 (October 17, 2019)
68
+
69
+ * Expose #sync, a method that enables calling sync-actions
70
+
71
+ *Patrick Koperwas*
72
+
73
+ * Addition of contribution guidelines to README
74
+
75
+ *Megan O'Neill*
76
+
77
+ * Fix TravisCI failures
78
+
79
+ *Patrick Koperwas*
80
+
81
+ ## 0.8.7 (April 18, 2019)
82
+
83
+ * Add api_version to allow calls to procore rest endpoints
84
+
85
+ *Shane Means*
2
86
 
3
87
  ## 0.8.6 (May 10, 2018)
4
88
 
@@ -7,7 +91,7 @@
7
91
  *Patrick Koperwas*
8
92
 
9
93
  * Fix Requestable paths to prevent double slash in URI
10
-
94
+
11
95
  *Megan O'Neill*
12
96
 
13
97
  ## 0.8.5 (May 9, 2018)
data/README.md CHANGED
@@ -4,12 +4,14 @@
4
4
 
5
5
  #### Table of Contents
6
6
  - [Installation](#installation)
7
+ - [1.0.0 Release](#100-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)
@@ -19,6 +21,7 @@
19
21
  - [File Store](#file-store)
20
22
  - [Memory Store](#memory-store)
21
23
  - [Full Example](#full-example)
24
+ - [Contributing](#contributing)
22
25
 
23
26
  ## Installation
24
27
 
@@ -28,6 +31,11 @@ Add this line to your application's Gemfile:
28
31
  gem "procore"
29
32
  ```
30
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
+
31
39
  ## Making Requests
32
40
 
33
41
  At the core of the gem is the `Client` class. Clients are initialized with a
@@ -39,19 +47,27 @@ Stores automatically manage tokens for you - refreshing, revoking and storage
39
47
  are abstracted away to make your code as simple as possible. There are several
40
48
  different [types of stores](#stores) available to you.
41
49
 
42
- The Client class exposes `#get`, `#post`, `#put`, `#patch` and `#delete` methods
43
- to you.
50
+ The Client class exposes `#get`, `#post`, `#put`, `#patch`, `#sync` and
51
+ `#delete` methods to you.
44
52
 
45
53
  ```ruby
46
- get(path, query: {})
47
- post(path, body: {}, options: {})
48
- put(path, body: {}, options: {})
49
- patch(path, body: {}, options: {})
50
- 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: {})
51
60
  ```
52
61
 
53
- All paths are relative - the gem will handle expanding `client.get("me")` to
54
- `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://api.procore.com/rest/v1.0/me` |
69
+ | `client.get("me", version: "v1.1")` | `https://api.procore.com/rest/v1.1/me` |
70
+ | `client.get("me", version: "vapid")` | `https://api.procore.com/vapid/me` |
55
71
 
56
72
  Example Usage:
57
73
 
@@ -69,6 +85,19 @@ companies = client.get("companies")
69
85
  companies.first[:name] #=> "Procore Company 1"
70
86
  ```
71
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
+
72
101
  ## Usage
73
102
 
74
103
  The first step is to place the user's token into the store. For this example,
@@ -112,6 +141,18 @@ client = Procore::Client.new(
112
141
  client.get("me")
113
142
  ```
114
143
 
144
+ Expired tokens will automatically be refreshed, but can also be refreshed manually:
145
+
146
+ ```ruby
147
+ client.refresh
148
+ ```
149
+
150
+ Tokens may also be manually revoked, forcing the client to refresh its token on the next request:
151
+
152
+ ```ruby
153
+ client.revoke
154
+ ```
155
+
115
156
  ## Error Handling
116
157
 
117
158
  The Procore Gem raises errors whenever a request returns a non `2xx` response.
@@ -259,6 +300,49 @@ puts first_page.pagination
259
300
  Notice that because `per_page` has been set to 250, there are only two pages of
260
301
  results (500 resources / 250 page size = 2 pages).
261
302
 
303
+ ## Sync Actions
304
+ The Sync action enables batch creation or updates to resources using a single
305
+ call. When using a Sync action, the resources to be created or updated can be
306
+ specified by supplying either an `id` or an `origin_id` in the request body.
307
+ Utilizing the `origin_id` attribute for batch operations is often preferable as
308
+ it allows you to easily link to external systems by maintaining your own list of
309
+ unique resource identifiers outside of Procore.
310
+
311
+ The caller provides an array of hashes, each hash containing the attributes for
312
+ a single resource. The attribute names in each hash match those used by the
313
+ Create and Update actions for the resource. Attributes for a maximum of 1000
314
+ resources within a collection may be passed with each call. The API will always
315
+ return an HTTP status of 200.
316
+
317
+ The response body contains two attributes - `entities` and `errors`. The
318
+ attributes for each successfully created or updated resource will appear in the
319
+ entities list. The attributes for each resource will match those returned by the
320
+ Show action. For each resource which could not be created or updated, the
321
+ attributes supplied by the caller are present in the errors list, along with an
322
+ additional errors attribute which provides reasons for the failure.
323
+
324
+ [Continue reading
325
+ here.](https://developers.procore.com/documentation/using-sync-actions)
326
+
327
+ Example Usage:
328
+
329
+ ```ruby
330
+ client.sync(
331
+ "projects/sync",
332
+ body: {
333
+ updates: [
334
+ { id: 1, name: "Update 1" },
335
+ { id: 2, name: "Update 2" },
336
+ { id: 3, name: "Update 3" },
337
+ ...
338
+ ...
339
+ { id: 5055, name: "Update 5055" },
340
+ ]
341
+ },
342
+ options: { batch_size: 500, company_id: 1 },
343
+ )
344
+ ```
345
+
262
346
  ## Configuration
263
347
 
264
348
  The Procore Gem exposes a configuration with several options.
@@ -271,7 +355,16 @@ Procore.configure do |config|
271
355
  # Base API host name. Alter this depending on your environment - in a
272
356
  # staging or test environment you may want to point this at a sandbox
273
357
  # instead of production.
274
- config.host = ENV.fetch("PROCORE_BASE_API_PATH", "https://app.procore.com")
358
+ config.host = ENV.fetch("PROCORE_BASE_API_PATH", "https://api.procore.com")
359
+
360
+ # When using #sync action, sets the default batch size to use for chunking
361
+ # up a request body. Example: if the size is set to 500, and 2,000 updates
362
+ # are desired, 4 requests will be made. Note, the maximum size is 1000.
363
+ config.default_batch_size = 500
364
+
365
+ # The default API version to use if none is specified in the request.
366
+ # Should be either "v1.0" (recommended) or "vapid" (legacy).
367
+ config.default_version = "v1.0"
275
368
 
276
369
  # Integer: Number of times to retry a failed API call. Reasons an API call
277
370
  # could potentially fail:
@@ -314,6 +407,14 @@ Options: `session`: Instance of a Rails session
314
407
 
315
408
  For applications that want to keep access tokens in the user's session.
316
409
 
410
+ :warning:
411
+ We strongly discourage using the session as a token store since the rails
412
+ session is often logged by default to external apps such as bugsnag etc. Be sure
413
+ you are not logging tokens. There is also the possibility that the rails session
414
+ is using a cookie store which, depending on application settings, could be
415
+ unencrypted. Tokens should not be stored client-side if it can be avoided.
416
+ :warning:
417
+
317
418
  ```ruby
318
419
  store = Procore::Auth::Stores::Session.new(session: session)
319
420
  ```
@@ -425,6 +526,27 @@ class ProjectsController
425
526
  end
426
527
  end
427
528
  ```
529
+ ## Contributing
530
+
531
+ 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:
532
+
533
+ ```markdown
534
+ ## Unreleased
535
+
536
+ * Short sentence of what has changed
537
+
538
+ *Your Name*
539
+ ```
540
+
541
+ Please **do not** bump the gem version in your PR. This will be done in a follow up PR by the gem maintainers.
542
+
543
+ ### Tests
544
+
545
+ To run the specs run the following command:
546
+ ```bash
547
+ $ bundle exec rake test
548
+ ```
549
+
428
550
 
429
551
  ## License
430
552
 
@@ -32,6 +32,17 @@ module Procore
32
32
  end
33
33
  end
34
34
 
35
+ def revoke(token:)
36
+ request = {
37
+ client_id: @client_id,
38
+ client_secret: @client_secret,
39
+ token: token.access_token,
40
+ }
41
+ client.request(:post, "/oauth/revoke", body: request)
42
+ rescue RestClient::ExceptionWithResponse
43
+ raise OAuthError.new(e.description, response: e.response)
44
+ end
45
+
35
46
  private
36
47
 
37
48
  def client
@@ -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",
@@ -38,19 +38,56 @@ module Procore
38
38
  @store = store
39
39
  end
40
40
 
41
+ # @raise [OAuthError] if a token cannot be refreshed.
42
+ def refresh
43
+ token = fetch_token
44
+
45
+ begin
46
+ new_token = @credentials.refresh(
47
+ token: token.access_token,
48
+ refresh: token.refresh_token,
49
+ )
50
+
51
+ Util.log_info("Token Refresh Successful", store: store)
52
+ store.save(new_token)
53
+ rescue RuntimeError
54
+ Util.log_error("Token Refresh Failed", store: store)
55
+ raise Procore::OAuthError.new(
56
+ "Unable to refresh the access token. Perhaps the Procore API is " \
57
+ "down or your access token store is misconfigured. Either " \
58
+ "way, you should clear the store and prompt the user to sign in " \
59
+ "again.",
60
+ )
61
+ end
62
+ end
63
+
64
+ # @raise [OAuthError] if a token cannot be revoked.
65
+ def revoke
66
+ token = fetch_token
67
+
68
+ begin
69
+ @credentials.revoke(token: token)
70
+ Util.log_info("Token Revocation Successful", store: store)
71
+ rescue RuntimeError
72
+ Util.log_error("Token Revocation Failed", store: store)
73
+ raise Procore::OAuthError.new(
74
+ "Unable to revoke the access token. Perhaps the Procore API is " \
75
+ "down or your access token store is misconfigured. Either " \
76
+ "way, you should clear the store and prompt the user to sign in " \
77
+ "again.",
78
+ )
79
+ end
80
+ end
81
+
41
82
  private
42
83
 
43
84
  def base_api_path
44
- "#{options[:host]}/vapid"
85
+ "#{options[:host]}"
45
86
  end
46
87
 
47
- # @raise [OAuthError] if the store does not have a token stored in it prior
48
- # to making a request.
49
- # @raise [OAuthError] if a token cannot be refreshed.
50
- # @raise [OAuthError] if incorrect credentials have been supplied.
51
- def access_token
88
+ # @raise [OAuthError] if the store does not have a token stored.
89
+ def fetch_token
52
90
  token = store.fetch
53
-
54
91
  if token.nil? || token.invalid?
55
92
  raise Procore::MissingTokenError.new(
56
93
  "Unable to retreive an access token from the store. Double check " \
@@ -58,28 +95,15 @@ module Procore
58
95
  "before attempting to make API requests",
59
96
  )
60
97
  end
98
+ token
99
+ end
61
100
 
101
+ def access_token
102
+ token = fetch_token
62
103
  if token.expired?
63
104
  Util.log_info("Token Expired", store: store)
64
- begin
65
- token = @credentials.refresh(
66
- token: token.access_token,
67
- refresh: token.refresh_token,
68
- )
69
-
70
- Util.log_info("Token Refresh Successful", store: store)
71
- store.save(token)
72
- rescue RuntimeError
73
- Util.log_error("Token Refresh Failed", store: store)
74
- raise Procore::OAuthError.new(
75
- "Unable to refresh the access token. Perhaps the Procore API is " \
76
- "down or the your access token store is misconfigured. Either " \
77
- "way, you should clear the store and prompt the user to sign in " \
78
- "again.",
79
- )
80
- end
105
+ refresh
81
106
  end
82
-
83
107
  token.access_token
84
108
  end
85
109
  end
@@ -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
@@ -4,15 +4,22 @@ module Procore
4
4
  # Specifies some sensible defaults for certain configurations + clients
5
5
  class Defaults
6
6
  # Default API endpoint
7
- API_ENDPOINT = "https://app.procore.com".freeze
7
+ API_ENDPOINT = "https://api.procore.com".freeze
8
8
 
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
@@ -19,6 +19,7 @@ module Procore
19
19
  RestClient::ServerBrokeConnection,
20
20
  ].freeze
21
21
  # @param path [String] URL path
22
+ # @param version [String] API version
22
23
  # @param query [Hash] Query options to pass along with the request
23
24
  # @option options [Hash] :company_id
24
25
  #
@@ -26,8 +27,8 @@ module Procore
26
27
  # client.get("my_open_items", query: { per_page: 5, filter: {} })
27
28
  #
28
29
  # @return [Response]
29
- def get(path, query: {}, options: {})
30
- full_path = full_path(path)
30
+ def get(path, version: nil, query: {}, options: {})
31
+ full_path = full_path(path, version)
31
32
 
32
33
  Util.log_info(
33
34
  "API Request Initiated",
@@ -47,8 +48,9 @@ module Procore
47
48
  end
48
49
 
49
50
  # @param path [String] URL path
51
+ # @param version [String] API version
50
52
  # @param body [Hash] Body parameters to send with the request
51
- # @param options [Hash} Extra request options
53
+ # @param options [Hash] Extra request options
52
54
  # @option options [String] :idempotency_token | :company_id
53
55
  #
54
56
  # @example Usage
@@ -59,8 +61,8 @@ module Procore
59
61
  # )
60
62
  #
61
63
  # @return [Response]
62
- def post(path, body: {}, options: {})
63
- full_path = full_path(path)
64
+ def post(path, version: nil, body: {}, options: {})
65
+ full_path = full_path(path, version)
64
66
 
65
67
  Util.log_info(
66
68
  "API Request Initiated",
@@ -81,16 +83,17 @@ module Procore
81
83
  end
82
84
 
83
85
  # @param path [String] URL path
86
+ # @param version [String] API version
84
87
  # @param body [Hash] Body parameters to send with the request
85
- # @param options [Hash} Extra request options
88
+ # @param options [Hash] Extra request options
86
89
  # @option options [String] :idempotency_token | :company_id
87
90
  #
88
91
  # @example Usage
89
92
  # client.put("dashboards/1/users", body: [1,2,3], options: { company_id: 1 })
90
93
  #
91
94
  # @return [Response]
92
- def put(path, body: {}, options: {})
93
- full_path = full_path(path)
95
+ def put(path, version: nil, body: {}, options: {})
96
+ full_path = full_path(path, version)
94
97
 
95
98
  Util.log_info(
96
99
  "API Request Initiated",
@@ -111,8 +114,9 @@ module Procore
111
114
  end
112
115
 
113
116
  # @param path [String] URL path
117
+ # @param version [String] API version
114
118
  # @param body [Hash] Body parameters to send with the request
115
- # @param options [Hash} Extra request options
119
+ # @param options [Hash] Extra request options
116
120
  # @option options [String] :idempotency_token | :company_id
117
121
  #
118
122
  # @example Usage
@@ -123,8 +127,8 @@ module Procore
123
127
  # )
124
128
  #
125
129
  # @return [Response]
126
- def patch(path, body: {}, options: {})
127
- full_path = full_path(path)
130
+ def patch(path, version: nil, body: {}, options: {})
131
+ full_path = full_path(path, version)
128
132
 
129
133
  Util.log_info(
130
134
  "API Request Initiated",
@@ -145,6 +149,73 @@ module Procore
145
149
  end
146
150
 
147
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
148
219
  # @param query [Hash] Query options to pass along with the request
149
220
  # @option options [String] :company_id
150
221
  #
@@ -152,8 +223,8 @@ module Procore
152
223
  # client.delete("users/1", query: {}, options: {})
153
224
  #
154
225
  # @return [Response]
155
- def delete(path, query: {}, options: {})
156
- full_path = full_path(path)
226
+ def delete(path, version: nil, query: {}, options: {})
227
+ full_path = full_path(path, version)
157
228
 
158
229
  Util.log_info(
159
230
  "API Request Initiated",
@@ -296,8 +367,15 @@ module Procore
296
367
  RestClient::Payload::has_file?(body)
297
368
  end
298
369
 
299
- def full_path(path)
300
- File.join(base_api_path, path).to_s
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 Procore::InvalidRequestError.new "#{version} is an invalid Procore API version"
378
+ end
301
379
  end
302
380
  end
303
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.6".freeze
2
+ VERSION = "1.1.1".freeze
3
3
  end
data/procore.gemspec CHANGED
@@ -32,6 +32,8 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "rake"
33
33
  spec.add_development_dependency "redis"
34
34
  spec.add_development_dependency "rubocop"
35
+ spec.add_development_dependency "rubocop-performance"
36
+ spec.add_development_dependency "rubocop-rails"
35
37
  spec.add_development_dependency "sqlite3"
36
38
  spec.add_development_dependency "webmock"
37
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.6
4
+ version: 1.1.1
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-10 00:00:00.000000000 Z
11
+ date: 2021-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -150,6 +150,34 @@ dependencies:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
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'
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: sqlite3
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -276,7 +304,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
276
304
  version: '0'
277
305
  requirements: []
278
306
  rubyforge_project:
279
- rubygems_version: 2.7.6
307
+ rubygems_version: 2.6.14
280
308
  signing_key:
281
309
  specification_version: 4
282
310
  summary: Procore Ruby Gem