workable 1.0.0 → 2.0.0rc1

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
  SHA1:
3
- metadata.gz: 9a4c28db1b5d41398db697d8d6e559a26524d952
4
- data.tar.gz: 67a276520623daa50fe9d07d5b9ab49d736702c2
3
+ metadata.gz: d886522592c7473bc8fb058f56ea68a8b2d79585
4
+ data.tar.gz: c019b51b0a908a38ac418fd2ea8003f98c5e9ca5
5
5
  SHA512:
6
- metadata.gz: c2994d3ef7f6d4d39ac0b76cac35aaf32e64096eed043502bb1363cb6983e8e8e33ffb9692b5ee090ff67d93e6ed79ab333d3f8a5add60935dd53ed082e74c79
7
- data.tar.gz: 4273e9026eebd361f80beee5422c292ec4299f59c22b1ebdf23e1f94d84428df3a428f5560bce909e9c1438520d0100a059e17903f1ae50678cd8c53683aaa35
6
+ metadata.gz: 43d116e2475036166516674d0efff67bb5f8e13bd3f05bb0c63373ae91398df068d6190ac99f19f9bc662cd0d86755c7d3fbc752cafff4e177adbe9d888c1e4f
7
+ data.tar.gz: d9007195f31b8075e86a63a07bd3836a75ae2d6d1cc06e0ceccc9661212630f49e7a0f7e3c1d444f027f1824e7567afb7efada341279759fc405d7d77cb37de8
@@ -1,3 +1,18 @@
1
+ ## 2.0.0rc1
2
+
3
+ Date: 2015-11-02
4
+
5
+ **Breaking change - switched to v3 API**
6
+
7
+ https://workable.readme.io/docs/whats-new-in-v3
8
+
9
+ - Collections returned by workable API are now paginated - that enforced introducing
10
+ new `Workable::Collection` class that holds `data` (jobs/candidates) and reference
11
+ to next page of results.
12
+
13
+ - `jobs` method does not set default `stage` argument - please specify it explicitly!
14
+ Also - now it accepts a hash as more parameters are available in v3 API
15
+
1
16
  ## 1.0.0
2
17
 
3
18
  Date: 2015-05-16
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- #ruby=ruby-2.2.2
3
+ # ruby=ruby-2.2.2
4
4
 
5
5
  # Specify your gem's dependencies in workable-client.gemspec
6
6
  gemspec
data/Guardfile CHANGED
@@ -1,7 +1,7 @@
1
1
  directories %w(lib spec)
2
2
 
3
- guard :rspec, cmd: "rspec" do
3
+ guard :rspec, cmd: 'rspec' do
4
4
  watch(%r{^spec/.+_spec\.rb$})
5
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
6
- watch(%r{spec/(spec_helper|fixtures)\.rb$}) { "spec" }
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
6
+ watch(%r{spec/(spec_helper|fixtures)\.rb$}) { 'spec' }
7
7
  end
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  Dead-simple Ruby API client for [workable.com][1]. No extra runtime dependencies. Ruby >= 1.9.3.
10
10
 
11
- Uses v2 API provided by workable.
11
+ Uses v3 API provided by workable.
12
12
 
13
13
  ## Installation
14
14
 
@@ -28,18 +28,17 @@ Or install it yourself as:
28
28
 
29
29
  ## Usage
30
30
 
31
- Internal interface / api is in early stage, so far you can:
32
- - fetch jobs
33
- - fetch job details
34
- - fetch candidates for given job
31
+ Gem covers all endpoints mentioned in official v3 workable API documentation (https://workable.readme.io/docs/).
35
32
 
36
33
  ### Example
37
34
 
35
+ For detailed documentation please refer to: http://www.rubydoc.info/gems/workable
36
+
38
37
  ``` ruby
39
38
  client = Workable::Client.new(api_key: 'api_key', subdomain: 'your_subdomain')
40
39
 
41
40
  # takes optional phase argument (string): 'published' (default), 'draft', 'closed' or 'archived'
42
- client.jobs # => Array of hashes
41
+ client.jobs # => Workable::Collection
43
42
 
44
43
  shortcode = client.jobs.first["shortcode"]
45
44
 
data/Rakefile CHANGED
@@ -1,10 +1,11 @@
1
- require "rspec/core/rake_task"
1
+ require 'rspec/core/rake_task'
2
+ require 'bundler/gem_tasks'
2
3
 
3
4
  RSpec::Core::RakeTask.new('spec')
4
5
  task default: :spec
5
6
 
6
7
  task :console do
7
- sh "irb -r ./lib/workable.rb"
8
+ sh 'irb -r ./lib/workable.rb'
8
9
  end
9
10
 
10
11
  task c: :console
@@ -3,11 +3,14 @@ require 'uri'
3
3
  require 'net/http'
4
4
  require 'date'
5
5
  require 'ostruct'
6
+ require 'cgi'
6
7
 
7
- require_relative "workable/version"
8
- require_relative "workable/errors"
9
- require_relative "workable/client"
8
+ require_relative 'workable/version'
9
+ require_relative 'workable/errors'
10
+ require_relative 'workable/client'
11
+ require_relative 'workable/transformation'
12
+ require_relative 'workable/collection'
10
13
 
11
14
  module Workable
12
- API_VERSION = "2".freeze
15
+ API_VERSION = 3
13
16
  end
@@ -1,13 +1,12 @@
1
1
  module Workable
2
2
  class Client
3
-
4
3
  # set access to workable and data transformation methods
5
4
  #
6
5
  # @param options [Hash]
7
6
  # @option options :api_key [String] api key for workable
8
7
  # @option options :subdomain [String] company subdomain in workable
9
8
  # @option options :transform_to [Hash<Symbol: Proc>] mapping of transformations for data
10
- # available transformations: [:job, :candidate, :question, :stage]
9
+ # available transformations: [:job, :candidate, :question, :stage, :recruiter, :member]
11
10
  # when no transformation is given raw Hash / Array data is returned
12
11
  #
13
12
  # @example transformation for candidates using `MyApp::Candidate.find_and_update_or_create`
@@ -29,60 +28,103 @@ module Workable
29
28
  # }
30
29
  # )
31
30
  def initialize(options = {})
32
- @api_key = options.fetch(:api_key) { fail Errors::InvalidConfiguration, "Missing api_key argument" }
33
- @subdomain = options.fetch(:subdomain) { fail Errors::InvalidConfiguration, "Missing subdomain argument" }
34
- @transform_to = options[:transform_to] || {}
35
- @transform_from = options[:transform_from] || {}
31
+ @api_key = options.fetch(:api_key) { configuration_error 'Missing api_key argument' }
32
+ @subdomain = options.fetch(:subdomain) { configuration_error 'Missing subdomain argument' }
33
+ @transform_to = Transformation.new(options[:transform_to])
34
+ @transform_from = Transformation.new(options[:transform_from])
35
+ end
36
+
37
+ # return information about your account
38
+ def about
39
+ get_request('')
40
+ end
41
+
42
+ # returns a collection of your account members
43
+ def members
44
+ @transform_to.apply(:member, get_request('members')['members'])
45
+ end
46
+
47
+ # returns a collection of your account external recruiters
48
+ def recruiters
49
+ @transform_to.apply(:recruiter, get_request('recruiters')['recruiters'])
50
+ end
51
+
52
+ # returns a collection of your recruitment pipeline stages
53
+ def stages
54
+ @transform_to.apply(:stage, get_request('stages')['stages'])
36
55
  end
37
56
 
38
- # request jobs of given type
39
- # @param type [String] type of jobs to fetch, `published` by default
40
- def jobs(type = 'published')
41
- transform_to(:job, get_request("jobs?phase=#{type}")['jobs'])
57
+ # request posted jobs
58
+ # @option params [Hash] optional filter parameters
59
+ # @option params :state [String] Returns jobs with the current state. Possible values (draft, published, archived & closed)
60
+ # @option params :limit [Integer] Specifies the number of jobs to try and retrieve per page
61
+ # @option params :since_id [String] Returns results with an ID more than or equal to the specified ID.
62
+ # @option params :max_id [String] Returns results with an ID less than or equal to the specified ID.
63
+ # @option params :created_after [Timestamp|Integer] Returns results created after the specified timestamp.
64
+ # @option params :updated_after [Timestamp|Integer] Returns results updated after the specified timestamp.
65
+ def jobs(params = {})
66
+ build_collection('jobs', :job, 'jobs', params)
42
67
  end
43
68
 
44
69
  # request detailed information about job
45
70
  # @param shortcode [String] job short code
46
71
  def job_details(shortcode)
47
- transform_to(:job, get_request("jobs/#{shortcode}"))
72
+ @transform_to.apply(:job, get_request("jobs/#{shortcode}"))
73
+ end
74
+
75
+ # list of questions for job
76
+ # @param shortcode [String] job short code
77
+ def job_questions(shortcode)
78
+ @transform_to.apply(:question, get_request("jobs/#{shortcode}/questions")['questions'])
79
+ end
80
+
81
+ # return a collection of job's members
82
+ # @param shortcode [String] job short code
83
+ def job_members(shortcode)
84
+ @transform_to.apply(:member, get_request("jobs/#{shortcode}/members")['members'])
85
+ end
86
+
87
+ # return a collection of the job's external recruiters
88
+ # @param shortcode [String] job short code
89
+ def job_recruiters(shortcode)
90
+ @transform_to.apply(:recruiter, get_request("jobs/#{shortcode}/recruiters")['recruiters'])
48
91
  end
49
92
 
50
93
  # list candidates for given job
51
94
  # @param shortcode [String] job shortcode to select candidates from
52
- # @param options [Hash] extra options like `stage_slug` or `limit`
53
- # @option options :stage [String] optional stage slug, if not given candidates are listed for all stages
54
- # @option options :limit [Number|String] optional limit of candidates to download, if not given all candidates are listed
55
- def job_candidates(shortcode, options = {})
56
- url = build_job_candidates_url(shortcode, options)
57
- transform_to(:candidate, get_request(url)['candidates'])
95
+ # @param params [Hash] extra options like `state` or `limit`
96
+ # @option params :state [String] optional state slug, if not given candidates are listed for all stages
97
+ # @option params :limit [Number|String] optional limit of candidates to download, if not given all candidates are listed
98
+ # @option params :since_id [String] Returns results with an ID more than or equal to the specified ID.
99
+ # @option params :max_id [String] Returns results with an ID less than or equal to the specified ID.
100
+ # @option params :created_after [Timestamp|Integer] Returns results created after the specified timestamp.
101
+ # @option params :updated_after [Timestamp|Integer] Returns results updated after the specified timestamp.
102
+ def job_candidates(shortcode, params = {})
103
+ build_collection("jobs/#{shortcode}/candidates", :candidate, 'candidates', params)
58
104
  end
59
105
 
60
- # list of questions for job
61
- # @param shortcode [String] job short code
62
- def job_questions(shortcode)
63
- transform_to(:question, get_request("jobs/#{shortcode}/questions")['questions'])
106
+ # return the full object of a specific candidate
107
+ # @param shortcode [String] job shortcode to select candidate from
108
+ # @param id [String] candidates's id
109
+ def job_candidate(shortcode, id)
110
+ @transform_to.apply(:candidate, get_request("jobs/#{shortcode}/candidates/#{id}")['candidate'])
64
111
  end
65
112
 
66
113
  # create new candidate for given job
67
114
  # @param candidate [Hash] the candidate data as described in
68
- # http://resources.workable.com/add-candidates-using-api
115
+ # https://workable.readme.io/docs/job-candidates-create
69
116
  # including the `{"candidate"=>{}}` part
70
117
  # @param shortcode [String] job short code
71
118
  # @param stage_slug [String] optional stage slug
72
119
  # @return [Hash] the candidate information without `{"candidate"=>{}}` part
73
120
  def create_job_candidate(candidate, shortcode, stage_slug = nil)
74
- shortcode = "#{shortcode}/#{stage_slug}" unless stage_slug.nil?
75
- transform_to(:candidate, post_request("jobs/#{shortcode}/candidates", candidate)["candidate"])
76
- end
121
+ shortcode = "#{shortcode}/#{stage_slug}" if stage_slug
77
122
 
78
- # list of stages defined for company
79
- def stages
80
- transform_to(:stage, get_request("stages")['stages'])
81
- end
123
+ response = post_request("jobs/#{shortcode}/candidates") do |request|
124
+ request.body = @transform_from.apply(:candidate, candidate).to_json
125
+ end
82
126
 
83
- # list of external recruiters for company
84
- def recruiters
85
- transform_to(:stage, get_request("recruiters")['recruiters'])
127
+ @transform_to.apply(:candidate, response['candidate'])
86
128
  end
87
129
 
88
130
  private
@@ -91,28 +133,30 @@ module Workable
91
133
 
92
134
  # build the url to api
93
135
  def api_url
94
- "https://www.workable.com/spi/v%s/accounts/%s" % [Workable::API_VERSION, subdomain]
136
+ 'https://www.workable.com/spi/v%s/accounts/%s' % [Workable::API_VERSION, subdomain]
95
137
  end
96
138
 
97
139
  # do the get request to api
98
- def get_request(url)
99
- do_request(url, Net::HTTP::Get)
140
+ def get_request(url, params = {})
141
+ params = URI.encode_www_form(params.keep_if { |k, v| k && v })
142
+ full_url = params.empty? ? url : [url, params].join('?')
143
+ do_request(full_url, Net::HTTP::Get)
100
144
  end
101
145
 
102
146
  # do the post request to api
103
- def post_request(url, data)
147
+ def post_request(url)
104
148
  do_request(url, Net::HTTP::Post) do |request|
105
- request.body = transform_from(:candidate, data).to_json
149
+ yield(request) if block_given?
106
150
  end
107
151
  end
108
152
 
109
153
  # generic part of requesting api
110
- def do_request(url, type, &block)
154
+ def do_request(url, type, &_block)
111
155
  uri = URI.parse("#{api_url}/#{url}")
112
156
  http = Net::HTTP.new(uri.host, uri.port)
113
157
  http.use_ssl = true
114
158
 
115
- request = type.new(uri.request_uri, headers)
159
+ request = type.new(uri.request_uri, headers)
116
160
  yield request if block_given?
117
161
  response = http.request(request)
118
162
 
@@ -127,28 +171,26 @@ module Workable
127
171
  when 200...300 # handled with response
128
172
  JSON.parse(response.body)
129
173
  when 401
130
- raise Errors::NotAuthorized, JSON.parse(response.body)["error"]
174
+ fail Errors::NotAuthorized, JSON.parse(response.body)['error']
131
175
  when 404
132
- raise Errors::NotFound, JSON.parse(response.body)["error"]
176
+ fail Errors::NotFound, JSON.parse(response.body)['error']
133
177
  when 422
134
178
  handle_response_422(response)
135
179
  when 503
136
- raise Errors::RequestToLong, response.body
180
+ fail Errors::RequestToLong, response.body
137
181
  else
138
- raise Errors::InvalidResponse, "Response code: #{response.code} message: #{response.body}"
182
+ fail Errors::InvalidResponse, "Response code: #{response.code} message: #{response.body}"
139
183
  end
140
184
  end
141
185
 
142
186
  def handle_response_422(response)
143
187
  data = JSON.parse(response.body)
144
- if
145
- data["validation_errors"] &&
146
- data["validation_errors"]["email"] &&
147
- data["validation_errors"]["email"].include?("candidate already exists")
148
- then
149
- raise Errors::AlreadyExists, data["error"]
188
+ if data['validation_errors'] &&
189
+ data['validation_errors']['email'] &&
190
+ data['validation_errors']['email'].include?('candidate already exists')
191
+ fail Errors::AlreadyExists, data['error']
150
192
  else
151
- raise Errors::NotFound, data["error"]
193
+ fail Errors::NotFound, data['error']
152
194
  end
153
195
  end
154
196
 
@@ -158,35 +200,10 @@ module Workable
158
200
  'Accept' => 'application/json',
159
201
  'Authorization' => "Bearer #{api_key}",
160
202
  'Content-Type' => 'application/json',
161
- 'User-Agent' => 'Workable Ruby Client',
203
+ 'User-Agent' => 'Workable Ruby Client'
162
204
  }
163
205
  end
164
206
 
165
- # build url for fetching job candidates
166
- # @param shortcode [String] job shortcode to select candidates from
167
- # @param options [Hash] extra options like `stage_slug` or `limit`
168
- # @option options :stage_slug [String] optional stage slug, if not given candidates are listed for all stages
169
- # @option options :limit [Number|String] optional limit of candidates to download, if not given all candidates are listed
170
- def build_job_candidates_url(shortcode, options)
171
- if (stage_slug = options.delete(:stage))
172
- then stage_slug = "/#{stage_slug}"
173
- end
174
- params =
175
- if options.empty?
176
- then ""
177
- else "?#{options.map{|k,v| "#{k}=#{v}"}.join("&")}"
178
- end
179
- "jobs/#{shortcode}#{stage_slug}/candidates#{params}"
180
- end
181
-
182
- # transform result using given method if defined
183
- # @param type [Symbol] type of the transformation, one of `[:job, :candidate, :question, :stage]`
184
- # @param result [Hash|Array|nil] the value to transform, can be nothing, `Hash` of values or `Array` of `Hash`es
185
- # @return transformed result if transformation exists for type, raw result otherwise
186
- def transform_to(type, result)
187
- transform(@transform_to[type], result)
188
- end
189
-
190
207
  # transform input using given method if defined
191
208
  # @param type [Symbol] type of the transformation, only `[:candidate]` supported so far
192
209
  # @param result [Hash|Array|nil] the value to transform, can be nothing, `Hash` of values or `Array` of `Hash`es
@@ -195,22 +212,20 @@ module Workable
195
212
  transform(@transform_from[type], input)
196
213
  end
197
214
 
198
- # selects transformation strategy based on the inputs
199
- # @param transformation [Method|Proc|nil] the transformation to perform
200
- # @param data [Hash|Array|nil] the data to transform
201
- # @return [Object|nil]
202
- # results of the transformation if given, raw data otherwise
203
- def transform(transformation, data)
204
- return data unless transformation
205
- case data
206
- when nil
207
- data
208
- when Array
209
- data.map{|datas| transformation.call(datas) }
210
- else
211
- transformation.call(data)
212
- end
215
+ def build_collection(url, transform_mapping, root_key, params = {})
216
+ url = url.gsub(/#{api_url}\/?/, '')
217
+ response = get_request(url, params)
218
+
219
+ Collection.new(
220
+ @transform_to.apply(transform_mapping, response[root_key]),
221
+ method(__callee__),
222
+ transform_mapping,
223
+ root_key,
224
+ response['paging'])
213
225
  end
214
226
 
227
+ def configuration_error(message)
228
+ fail Errors::InvalidConfiguration, message
229
+ end
215
230
  end
216
231
  end
@@ -0,0 +1,29 @@
1
+ module Workable
2
+ class Collection
3
+ extend Forwardable
4
+ def_delegators :@data, :size, :each, :[], :map, :first
5
+
6
+ attr_reader :data
7
+
8
+ def initialize(data, next_page_method, transform_mapping, root_key, paging = nil)
9
+ @data = data
10
+
11
+ if paging
12
+ @next_page = paging['next']
13
+ @next_page_method = next_page_method
14
+ @transform_mapping = transform_mapping
15
+ @root_key = root_key
16
+ end
17
+ end
18
+
19
+ def next_page?
20
+ !! @next_page
21
+ end
22
+
23
+ def fetch_next_page
24
+ return unless next_page?
25
+
26
+ @next_page_method.call(@next_page, @transform_mapping, @root_key)
27
+ end
28
+ end
29
+ end
@@ -1,11 +1,11 @@
1
1
  module Workable
2
2
  module Errors
3
- class WorkableError < StandardError; end
3
+ class WorkableError < StandardError; end
4
4
  class InvalidConfiguration < WorkableError; end
5
- class NotAuthorized < WorkableError; end
6
- class InvalidResponse < WorkableError; end
7
- class NotFound < WorkableError; end
8
- class AlreadyExists < WorkableError; end
9
- class RequestToLong < WorkableError; end
5
+ class NotAuthorized < WorkableError; end
6
+ class InvalidResponse < WorkableError; end
7
+ class NotFound < WorkableError; end
8
+ class AlreadyExists < WorkableError; end
9
+ class RequestToLong < WorkableError; end
10
10
  end
11
11
  end