workable 1.0.0 → 2.0.0rc1

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.
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