people_doc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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