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 +4 -4
- data/CHANGELOG.md +15 -0
- data/Gemfile +1 -1
- data/Guardfile +3 -3
- data/README.md +5 -6
- data/Rakefile +3 -2
- data/lib/workable.rb +7 -4
- data/lib/workable/client.rb +106 -91
- data/lib/workable/collection.rb +29 -0
- data/lib/workable/errors.rb +6 -6
- data/lib/workable/transformation.rb +26 -0
- data/lib/workable/version.rb +1 -1
- data/spec/fixtures.rb +34 -122
- data/spec/fixtures/about.json +8 -0
- data/spec/fixtures/job.json +31 -0
- data/spec/fixtures/job_candidate.json +119 -0
- data/spec/fixtures/job_candidates.json +58 -0
- data/spec/fixtures/job_questions.json +34 -0
- data/spec/fixtures/jobs.json +73 -0
- data/spec/fixtures/members.json +32 -0
- data/spec/fixtures/new_candidate.json +60 -0
- data/spec/fixtures/new_candidate_response.json +109 -0
- data/spec/fixtures/recruiters.json +14 -0
- data/spec/fixtures/stages.json +40 -0
- data/spec/lib/workable/client_spec.rb +157 -121
- data/spec/lib/workable/collection_spec.rb +49 -0
- data/spec/lib/workable/transformation_spec.rb +38 -0
- data/spec/spec_helper.rb +6 -1
- data/workable.gemspec +7 -7
- metadata +32 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d886522592c7473bc8fb058f56ea68a8b2d79585
|
4
|
+
data.tar.gz: c019b51b0a908a38ac418fd2ea8003f98c5e9ca5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43d116e2475036166516674d0efff67bb5f8e13bd3f05bb0c63373ae91398df068d6190ac99f19f9bc662cd0d86755c7d3fbc752cafff4e177adbe9d888c1e4f
|
7
|
+
data.tar.gz: d9007195f31b8075e86a63a07bd3836a75ae2d6d1cc06e0ceccc9661212630f49e7a0f7e3c1d444f027f1824e7567afb7efada341279759fc405d7d77cb37de8
|
data/CHANGELOG.md
CHANGED
@@ -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
data/Guardfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
directories %w(lib spec)
|
2
2
|
|
3
|
-
guard :rspec, cmd:
|
3
|
+
guard :rspec, cmd: 'rspec' do
|
4
4
|
watch(%r{^spec/.+_spec\.rb$})
|
5
|
-
watch(%r{^lib/(.+)\.rb$})
|
6
|
-
watch(%r{spec/(spec_helper|fixtures)\.rb$})
|
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
|
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
|
-
|
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 # =>
|
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
|
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
|
8
|
+
sh 'irb -r ./lib/workable.rb'
|
8
9
|
end
|
9
10
|
|
10
11
|
task c: :console
|
data/lib/workable.rb
CHANGED
@@ -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
|
8
|
-
require_relative
|
9
|
-
require_relative
|
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 =
|
15
|
+
API_VERSION = 3
|
13
16
|
end
|
data/lib/workable/client.rb
CHANGED
@@ -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) {
|
33
|
-
@subdomain = options.fetch(:subdomain) {
|
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
|
39
|
-
# @
|
40
|
-
|
41
|
-
|
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
|
53
|
-
# @option
|
54
|
-
# @option
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
#
|
61
|
-
# @param shortcode [String] job
|
62
|
-
|
63
|
-
|
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
|
-
#
|
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}"
|
75
|
-
transform_to(:candidate, post_request("jobs/#{shortcode}/candidates", candidate)["candidate"])
|
76
|
-
end
|
121
|
+
shortcode = "#{shortcode}/#{stage_slug}" if stage_slug
|
77
122
|
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
147
|
+
def post_request(url)
|
104
148
|
do_request(url, Net::HTTP::Post) do |request|
|
105
|
-
request
|
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, &
|
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
|
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
|
-
|
174
|
+
fail Errors::NotAuthorized, JSON.parse(response.body)['error']
|
131
175
|
when 404
|
132
|
-
|
176
|
+
fail Errors::NotFound, JSON.parse(response.body)['error']
|
133
177
|
when 422
|
134
178
|
handle_response_422(response)
|
135
179
|
when 503
|
136
|
-
|
180
|
+
fail Errors::RequestToLong, response.body
|
137
181
|
else
|
138
|
-
|
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
|
-
|
146
|
-
|
147
|
-
data[
|
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
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
data/lib/workable/errors.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
module Workable
|
2
2
|
module Errors
|
3
|
-
class WorkableError
|
3
|
+
class WorkableError < StandardError; end
|
4
4
|
class InvalidConfiguration < WorkableError; end
|
5
|
-
class NotAuthorized
|
6
|
-
class InvalidResponse
|
7
|
-
class NotFound
|
8
|
-
class AlreadyExists
|
9
|
-
class RequestToLong
|
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
|