people_doc 0.1.0

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.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ [![CircleCI](https://circleci.com/gh/westernmilling/people_doc.svg?style=svg&circle-token=f5b3c58d525d1d975f632b779221c1588cfeba97)](https://circleci.com/gh/westernmilling/people_doc)
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/6ade98502a90fe627e29/maintainability)](https://codeclimate.com/github/westernmilling/people_doc/maintainability)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/6ade98502a90fe627e29/test_coverage)](https://codeclimate.com/github/westernmilling/people_doc/test_coverage)
4
+
5
+ # PeopleDoc
6
+ Basic Ruby client library for the PeopleDoc REST APIs
7
+
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'people_doc'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install people_doc
24
+
25
+ ## Usage
26
+
27
+ ### RESTv1
28
+
29
+ ```ruby
30
+ client = PeopleDoc::V1::Client.new(
31
+ api_key: 'api_key'
32
+ base_url: 'https://api.staging.us.people-doc.com'
33
+ logger: Logger.new(STDOUT)
34
+ )
35
+
36
+ response = client.get('employees/12345678')
37
+ ```
38
+
39
+ ### RESTv2
40
+
41
+ ```ruby
42
+ client = PeopleDoc::V2::Client.new(
43
+ config.application_id = 'application_id'
44
+ config.application_secret = 'application_secret'
45
+ config.base_url = 'https://apis.staging.us.people-doc.com'
46
+ config.client_id = 'client_id'
47
+ config.logger = Logger.new(STDOUT)
48
+ )
49
+
50
+ response = client.get('organizations/ABC')
51
+ ```
52
+
53
+ ## Supported Functionality
54
+
55
+ - RESTv1
56
+ - GET
57
+ - POST
58
+ - POST with file
59
+
60
+ - RESTv2
61
+ - GET
62
+ - POST
63
+ - PUT
64
+
65
+ ## Development
66
+
67
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
68
+
69
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
70
+
71
+ ## Contributing
72
+
73
+ Bug reports and pull requests are welcome on GitHub at https://github.com/westernmilling/people_doc. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
74
+
75
+ ## License
76
+
77
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
78
+
79
+ ## Code of Conduct
80
+
81
+ Everyone interacting in the PeopleDoc client project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/westernmilling/people_doc/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %w(spec rubocop)
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "adp_client"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'people_doc/response_handlers'
5
+
6
+ module PeopleDoc
7
+ class HTTPartyRequest
8
+ def initialize(base_url, logger, response_handlers = [])
9
+ @base_url = base_url
10
+ @logger = logger
11
+ @response_handlers = response_handlers
12
+ end
13
+
14
+ def get(headers, resource)
15
+ url = "#{@base_url}/#{resource}"
16
+
17
+ @logger.debug("GET request Url: #{url}")
18
+ @logger.debug("-- Headers: #{headers}")
19
+
20
+ raises_unless_success do
21
+ HTTParty
22
+ .get(url, headers: headers)
23
+ end.parsed_response
24
+ end
25
+
26
+ def perform_request(headers, resource, payload)
27
+ http_method = __callee__
28
+ url = "#{@base_url}/#{resource}"
29
+
30
+ @logger.debug("#{http_method.upcase} request Url: #{url}")
31
+ @logger.debug("-- Headers: #{headers}")
32
+ @logger.debug("-- Payload: #{payload}")
33
+
34
+ raises_unless_success do
35
+ HTTParty
36
+ .send(http_method.to_sym, url, body: payload, headers: headers)
37
+ end.parsed_response
38
+ end
39
+ alias_method :put, :perform_request
40
+ alias_method :post, :perform_request
41
+
42
+ def post_file(headers, resource, payload)
43
+ http_method = 'post'
44
+ url = "#{@base_url}/#{resource}"
45
+
46
+ @logger.debug("#{http_method.upcase} request Url: #{url} (POST file)")
47
+ @logger.debug("-- Headers: #{headers}")
48
+ @logger.debug("-- Payload: #{payload}")
49
+
50
+ raises_unless_success do
51
+ HTTMultiParty
52
+ .send(http_method.to_sym, url, body: payload, headers: headers)
53
+ end.parsed_response
54
+ end
55
+
56
+ protected
57
+
58
+ def raises_unless_success
59
+ httparty = yield
60
+
61
+ @response_handlers.each_value do |handler_type|
62
+ handler_type.new(httparty).call
63
+ end
64
+
65
+ httparty
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+ module PeopleDoc
3
+ module ResponseHandlers
4
+ class BaseHandler
5
+ def initialize(httparty)
6
+ @httparty = httparty
7
+ end
8
+ end
9
+
10
+ class HandleBadRequest < BaseHandler
11
+ def call
12
+ fail BadRequest.new(@httparty.body, @httparty.parsed_response) \
13
+ if @httparty.code == 400
14
+ end
15
+ end
16
+
17
+ class HandleNotFound < BaseHandler
18
+ def call
19
+ fail NotFound.new(@httparty.body) if @httparty.code == 404
20
+ end
21
+ end
22
+
23
+ class HandleUnauthorized < BaseHandler
24
+ def call
25
+ fail Unauthorized.new(@httparty.body) \
26
+ if [401, 403].include?(@httparty.code)
27
+ end
28
+ end
29
+
30
+ class HandleUnprocessableEntity < BaseHandler
31
+ def call
32
+ return unless @httparty.code == 422
33
+
34
+ message = format(
35
+ '%<code>s: %<message>s',
36
+ code: @httparty.parsed_response['code'],
37
+ message: @httparty.parsed_response['message']
38
+ )
39
+ message += "\r\n\r\n"
40
+ message += @httparty
41
+ .parsed_response['errors']
42
+ .map { |error| "#{error['field']} - #{error['message']}" }
43
+ .join("\r\n")
44
+
45
+ fail UnprocessableEntity.new(message, @httparty.parsed_response)
46
+ end
47
+ end
48
+
49
+ class HandleUnknownFailure < BaseHandler
50
+ def call
51
+ fail HTTParty::Error.new(
52
+ "Code #{@httparty.code} - #{@httparty.body}"
53
+ ) unless @httparty.response.is_a?(Net::HTTPSuccess)
54
+ end
55
+ end
56
+
57
+ module V1
58
+ class HandleBadRequest < PeopleDoc::ResponseHandlers::BaseHandler
59
+ def call
60
+ return unless @httparty.code == 400
61
+
62
+ message = if @httparty.parsed_response['errors']
63
+ ErrorsResponse.new(@httparty).call
64
+ elsif @httparty.parsed_response['message']
65
+ MessageResponse.new(@httparty).call
66
+ else
67
+ @httparty.body
68
+ end
69
+
70
+ fail BadRequest.new(message, @httparty.parsed_response)
71
+ end
72
+
73
+ class ErrorsResponse
74
+ def initialize(response)
75
+ @response = response
76
+ end
77
+
78
+ def call
79
+ @response
80
+ .parsed_response['errors']
81
+ .map { |error| error['msg'] }
82
+ .join("\r\n")
83
+ end
84
+ end
85
+
86
+ class MessageResponse
87
+ def initialize(response)
88
+ @response = response
89
+ end
90
+
91
+ def call
92
+ @response.parsed_response['message']
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ module V2
99
+ class HandleBadRequest < PeopleDoc::ResponseHandlers::BaseHandler
100
+ def call
101
+ return unless @httparty.code == 400
102
+
103
+ message = @httparty.parsed_response['error_description']
104
+
105
+ fail BadRequest.new(message, @httparty.parsed_response)
106
+ end
107
+ end
108
+
109
+ class HandleUnauthorized < BaseHandler
110
+ def call
111
+ return unless @httparty.code == 401
112
+
113
+ message = if @httparty.parsed_response['error']
114
+ format(
115
+ '%<error>s: %<description>s',
116
+ error: @httparty.parsed_response['error'],
117
+ description: @httparty
118
+ .parsed_response['error_description']
119
+ )
120
+ elsif @httparty.parsed_response['code']
121
+ format(
122
+ '%<code>s: %<message>s',
123
+ code: @httparty.parsed_response['code'],
124
+ message: @httparty.parsed_response['message']
125
+ )
126
+ else
127
+ @httparty.body
128
+ end
129
+
130
+ fail Unauthorized.new(message)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PeopleDoc
4
+ VERSION = '0.1.0'
5
+ end
data/lib/people_doc.rb ADDED
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+ # $LOAD_PATH.unshift File.dirname(__FILE__)
3
+
4
+ require 'people_doc/version'
5
+ require 'people_doc/httparty_request'
6
+ require 'logger'
7
+
8
+ module PeopleDoc
9
+ RESPONSE_HANDLERS = {
10
+ bad_request: ResponseHandlers::HandleBadRequest,
11
+ not_found: ResponseHandlers::HandleNotFound,
12
+ unauthorized: ResponseHandlers::HandleUnauthorized,
13
+ unprocessable_entity: ResponseHandlers::HandleUnprocessableEntity,
14
+ unknown: ResponseHandlers::HandleUnknownFailure
15
+ }.freeze
16
+
17
+ class BaseError < StandardError
18
+ attr_reader :response
19
+
20
+ def initialize(message = nil, response = nil)
21
+ super(message)
22
+
23
+ @response = response
24
+ end
25
+ end
26
+ class BadRequest < BaseError; end
27
+ class NotFound < BaseError; end
28
+ class Unauthorized < BaseError; end
29
+ class UnprocessableEntity < BadRequest; end
30
+ class Token
31
+ attr_accessor :access_token, :token_type, :expires_in
32
+
33
+ def initialize(access_token, token_type, expires_in)
34
+ @access_token = access_token
35
+ @token_type = token_type
36
+ @expires_in = expires_in
37
+ end
38
+ end
39
+
40
+ module V1
41
+ class << self
42
+ attr_accessor :api_key,
43
+ :base_url,
44
+ :logger
45
+
46
+ ##
47
+ # Configures default PeopleDoc REST APIv1 settings.
48
+ #
49
+ # @example configuring the client defaults
50
+ # PeopleDoc::V1.configure do |config|
51
+ # config.api_key = 'api_key'
52
+ # config.base_url = 'https://api.staging.us.people-doc.com'
53
+ # config.logger = Logger.new(STDOUT)
54
+ # end
55
+ #
56
+ # @example using the client
57
+ # client = PeopleDoc::V1::Client.new
58
+ def configure
59
+ yield self
60
+ true
61
+ end
62
+ end
63
+
64
+ ##
65
+ # PeopleDoc REST API v1 Client
66
+ class Client
67
+ def initialize(options = {})
68
+ options = default_options.merge(options)
69
+
70
+ @api_key = options.fetch(:api_key)
71
+ @base_url = options.fetch(:base_url)
72
+ @logger = options.fetch(:logger, Logger.new(STDOUT))
73
+ @request = HTTPartyRequest.new(@base_url, @logger, response_handlers)
74
+ end
75
+
76
+ ##
77
+ # Get a resource
78
+ # Makes a request for a resource from PeopleDoc and returns the response
79
+ # as a raw {Hash}.
80
+ #
81
+ # @param [String] the resource endpoint
82
+ # @return [Hash] response data
83
+ def get(resource)
84
+ @request
85
+ .get(base_headers, "api/v1/#{resource}")
86
+ rescue NotFound
87
+ nil
88
+ end
89
+
90
+ ##
91
+ # POST a resource
92
+ # Makes a request to post new or existing resource details to PeopleDoc.
93
+ #
94
+ # @param [String] the resource endpoint
95
+ # @param [Hash] payload data
96
+ # @return [Hash] response data
97
+ def post(resource, payload)
98
+ @request.post(
99
+ base_headers,
100
+ "api/v1/#{resource}/",
101
+ payload.to_json
102
+ )
103
+ end
104
+
105
+ ##
106
+ # POST a file
107
+ # ...
108
+ #
109
+ # @param [String] the resource endpoint
110
+ # @param [...] file
111
+ # @param [Hash] payload data
112
+ # @return [Hash] response data
113
+ def post_file(resource, file, payload)
114
+ @request.post_file(
115
+ base_headers.merge(
116
+ 'Content-Type' => 'multipart/form-data'
117
+ ),
118
+ "api/v1/#{resource}/",
119
+ file: file,
120
+ data: payload.to_json
121
+ )
122
+ end
123
+
124
+ protected
125
+
126
+ def base_headers
127
+ {
128
+ 'Accept' => 'application/json',
129
+ 'X-API-KEY' => @api_key,
130
+ 'Content-Type' => 'application/json',
131
+ 'Host' => uri.host,
132
+ 'User-Agent' => 'PeopleDoc::V1::Client'
133
+ }
134
+ end
135
+
136
+ ##
137
+ # Default options
138
+ # A {Hash} of default options populate by attributes set during
139
+ # configuration.
140
+ #
141
+ # @return [Hash] containing the default options
142
+ def default_options
143
+ {
144
+ api_key: PeopleDoc::V1.api_key,
145
+ base_url: PeopleDoc::V1.base_url,
146
+ logger: PeopleDoc::V1.logger
147
+ }
148
+ end
149
+
150
+ def response_handlers
151
+ RESPONSE_HANDLERS.merge(
152
+ bad_request: PeopleDoc::ResponseHandlers::V1::HandleBadRequest
153
+ )
154
+ end
155
+
156
+ def uri
157
+ @uri ||= URI.parse(@base_url)
158
+ end
159
+ end
160
+ end
161
+
162
+ module V2
163
+ class << self
164
+ attr_accessor :application_id,
165
+ :application_secret,
166
+ :base_url,
167
+ :client_id,
168
+ :logger
169
+
170
+ ##
171
+ # Configures default PeopleDoc REST APIv1 settings.
172
+ #
173
+ # @example configuring the client defaults
174
+ # PeopleDoc::V2.configure do |config|
175
+ # config.application_id = 'application_id'
176
+ # config.application_secret = 'application_secret'
177
+ # config.base_url = 'https://apis.staging.us.people-doc.com'
178
+ # config.client_id = 'client_id'
179
+ # config.logger = Logger.new(STDOUT)
180
+ # end
181
+ #
182
+ # @example using the client
183
+ # client = PeopleDoc::V2::Client.new
184
+ def configure
185
+ yield self
186
+ true
187
+ end
188
+ end
189
+
190
+ ##
191
+ # PeopleDoc REST API v2 Client
192
+ class Client
193
+ def initialize(options = {})
194
+ options = default_options.merge(options)
195
+
196
+ @application_id = options.fetch(:application_id)
197
+ @application_secret = options.fetch(:application_secret)
198
+ @base_url = options.fetch(:base_url)
199
+ @client_id = options.fetch(:client_id)
200
+ @logger = options.fetch(:logger, Logger.new(STDOUT))
201
+ @request = HTTPartyRequest.new(@base_url, @logger, response_handlers)
202
+ end
203
+
204
+ def encoded_credentials
205
+ EncodedCredentials.new(@application_id, @application_secret).call
206
+ end
207
+
208
+ ##
209
+ # OAuth token
210
+ # Performs authentication using client credentials against the
211
+ # PeopleDoc Api.
212
+ #
213
+ # @return [Token] token details
214
+ def token
215
+ return @token if @token
216
+
217
+ payload = {
218
+ client_id: @client_id,
219
+ grant_type: 'client_credentials',
220
+ scope: 'client'
221
+ }
222
+ headers = {
223
+ 'Accept' => 'application/json',
224
+ 'Authorization' => "Basic #{encoded_credentials}",
225
+ 'Content-Type' => 'application/x-www-form-urlencoded',
226
+ 'Host' => uri.host,
227
+ 'User-Agent' => 'PeopleDoc::V2::Client'
228
+ }
229
+
230
+ response = @request.post(headers, 'api/v2/client/tokens', payload)
231
+
232
+ @token = Token.new(
233
+ *response.values_at('access_token', 'token_type', 'expires_in')
234
+ )
235
+ end
236
+
237
+ ##
238
+ # Get a resource
239
+ # Makes a request for a resource from PeopleDoc and returns the response
240
+ # as a raw {Hash}.
241
+ #
242
+ # @param [String] the resource endpoint
243
+ # @return [Hash] response data
244
+ def get(resource)
245
+ @request
246
+ .get(base_headers, "api/v2/client/#{resource}")
247
+ rescue NotFound
248
+ nil
249
+ end
250
+
251
+ ##
252
+ # POST a file
253
+ # ...
254
+ #
255
+ # @param [String] the resource endpoint
256
+ # @param [...] file
257
+ # @param [Hash] payload data
258
+ # @return [Hash] response data
259
+ def post_file(resource, file, payload = nil)
260
+ @request.post_file(
261
+ base_headers.merge(
262
+ 'Content-Type' => 'multipart/form-data'
263
+ ),
264
+ "api/v2/#{resource}",
265
+ file: file,
266
+ data: payload ? payload.to_json : nil
267
+ )
268
+ end
269
+
270
+ ##
271
+ # PUT a resource
272
+ # Makes a request to PUT new or existing resource details to PeopleDoc.
273
+ #
274
+ # @param [String] the resource endpoint
275
+ # @param [Hash] payload data
276
+ # @return [Hash] response data
277
+ def put(resource, payload)
278
+ @request.put(
279
+ base_headers,
280
+ "api/v2/client/#{resource}",
281
+ payload.to_json
282
+ )
283
+ end
284
+
285
+ protected
286
+
287
+ def base_headers
288
+ {
289
+ 'Accept' => 'application/json',
290
+ 'Authorization' => "Bearer #{token.access_token}",
291
+ 'Content-Type' => 'application/json',
292
+ 'Host' => uri.host,
293
+ 'User-Agent' => 'PeopleDoc::V2::Client'
294
+ }
295
+ end
296
+
297
+ ##
298
+ # Default options
299
+ # A {Hash} of default options populate by attributes set during
300
+ # configuration.
301
+ #
302
+ # @return [Hash] containing the default options
303
+ def default_options
304
+ {
305
+ application_id: PeopleDoc::V2.application_id,
306
+ application_secret: PeopleDoc::V2.application_secret,
307
+ base_url: PeopleDoc::V2.base_url,
308
+ client_id: PeopleDoc::V2.client_id,
309
+ logger: PeopleDoc::V2.logger
310
+ }
311
+ end
312
+
313
+ def response_handlers
314
+ RESPONSE_HANDLERS.merge(
315
+ bad_request: PeopleDoc::ResponseHandlers::V2::HandleBadRequest,
316
+ unauthorized: PeopleDoc::ResponseHandlers::V2::HandleUnauthorized
317
+ )
318
+ end
319
+
320
+ def uri
321
+ @uri ||= URI.parse(@base_url)
322
+ end
323
+ end
324
+
325
+ class EncodedCredentials
326
+ def initialize(application_id, application_secret)
327
+ @application_id = application_id
328
+ @application_secret = application_secret
329
+ end
330
+
331
+ def call
332
+ Base64.strict_encode64("#{@application_id}:#{@application_secret}")
333
+ end
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'people_doc/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'people_doc'
9
+ spec.version = PeopleDoc::VERSION
10
+ spec.authors = ['Joseph Bridgwater-Rowe']
11
+ spec.email = ['joe@westernmilling.com']
12
+ spec.summary = 'Basic PeopleDoc REST API client'
13
+ spec.homepage = 'https://github.com/westernmilling/people_doc'
14
+ spec.license = 'MIT'
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+ spec.add_runtime_dependency 'httparty'
22
+ spec.add_development_dependency 'bundler', '~> 1.16'
23
+ spec.add_development_dependency 'factory_bot'
24
+ spec.add_development_dependency 'faker'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec', '~> 3.0'
27
+ spec.add_development_dependency 'rspec_junit_formatter'
28
+ spec.add_development_dependency 'rubocop', '0.54.0'
29
+ spec.add_development_dependency 'simplecov'
30
+ spec.add_development_dependency 'webmock'
31
+ end