linkedin-drspin 0.3.6

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.
Files changed (58) hide show
  1. data/.autotest +14 -0
  2. data/.document +5 -0
  3. data/.gemtest +0 -0
  4. data/.gitignore +41 -0
  5. data/.rspec +1 -0
  6. data/.travis.yml +5 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE +20 -0
  9. data/README.markdown +78 -0
  10. data/Rakefile +19 -0
  11. data/changelog.markdown +71 -0
  12. data/examples/authenticate.rb +21 -0
  13. data/examples/network.rb +12 -0
  14. data/examples/profile.rb +18 -0
  15. data/examples/sinatra.rb +77 -0
  16. data/examples/status.rb +6 -0
  17. data/lib/linked_in/api.rb +11 -0
  18. data/lib/linked_in/api/comment_methods.rb +33 -0
  19. data/lib/linked_in/api/company_search_methods.rb +65 -0
  20. data/lib/linked_in/api/group_methods.rb +18 -0
  21. data/lib/linked_in/api/people_search_methods.rb +112 -0
  22. data/lib/linked_in/api/post_methods.rb +33 -0
  23. data/lib/linked_in/api/query_methods.rb +81 -0
  24. data/lib/linked_in/api/update_methods.rb +55 -0
  25. data/lib/linked_in/client.rb +51 -0
  26. data/lib/linked_in/errors.rb +19 -0
  27. data/lib/linked_in/helpers.rb +6 -0
  28. data/lib/linked_in/helpers/authorization.rb +68 -0
  29. data/lib/linked_in/helpers/request.rb +80 -0
  30. data/lib/linked_in/mash.rb +68 -0
  31. data/lib/linked_in/search.rb +56 -0
  32. data/lib/linked_in/version.rb +11 -0
  33. data/lib/linkedin.rb +32 -0
  34. data/linkedin.gemspec +25 -0
  35. data/spec/cases/api_spec.rb +92 -0
  36. data/spec/cases/linkedin_spec.rb +37 -0
  37. data/spec/cases/mash_spec.rb +85 -0
  38. data/spec/cases/oauth_spec.rb +166 -0
  39. data/spec/cases/search_spec.rb +206 -0
  40. data/spec/fixtures/cassette_library/LinkedIn_Api/Company_API.yml +73 -0
  41. data/spec/fixtures/cassette_library/LinkedIn_Client/_authorize_from_request.yml +28 -0
  42. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token.yml +28 -0
  43. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_a_callback_url.yml +28 -0
  44. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_default_options.yml +28 -0
  45. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_company_name_option.yml +135 -0
  46. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options.yml +122 -0
  47. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options_with_fields.yml +72 -0
  48. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_keywords_string_parameter.yml +136 -0
  49. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option.yml +136 -0
  50. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option_with_pagination.yml +128 -0
  51. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_options_with_fields.yml +252 -0
  52. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_keywords_string_parameter.yml +73 -0
  53. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option.yml +71 -0
  54. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_a_facet.yml +111 -0
  55. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_facets_to_return.yml +71 -0
  56. data/spec/fixtures/cassette_library/LinkedIn_Search/_search_company/by_single_keywords_option_with_pagination.yml +66 -0
  57. data/spec/helper.rb +30 -0
  58. metadata +237 -0
@@ -0,0 +1,68 @@
1
+ module LinkedIn
2
+ module Helpers
3
+
4
+ module Authorization
5
+
6
+ DEFAULT_OAUTH_OPTIONS = {
7
+ :request_token_path => "/uas/oauth/requestToken",
8
+ :access_token_path => "/uas/oauth/accessToken",
9
+ :authorize_path => "/uas/oauth/authorize",
10
+ :api_host => "https://api.linkedin.com",
11
+ :auth_host => "https://www.linkedin.com"
12
+ }
13
+
14
+ def consumer
15
+ @consumer ||= ::OAuth::Consumer.new(@consumer_token, @consumer_secret, parse_oauth_options)
16
+ end
17
+
18
+ # Note: If using oauth with a web app, be sure to provide :oauth_callback.
19
+ # Options:
20
+ # :oauth_callback => String, url that LinkedIn should redirect to
21
+ def request_token(options={})
22
+ @request_token ||= consumer.get_request_token(options)
23
+ end
24
+
25
+ # For web apps use params[:oauth_verifier], for desktop apps,
26
+ # use the verifier is the pin that LinkedIn gives users.
27
+ def authorize_from_request(request_token, request_secret, verifier_or_pin)
28
+ request_token = ::OAuth::RequestToken.new(consumer, request_token, request_secret)
29
+ access_token = request_token.get_access_token(:oauth_verifier => verifier_or_pin)
30
+ @auth_token, @auth_secret = access_token.token, access_token.secret
31
+ end
32
+
33
+ def access_token
34
+ @access_token ||= ::OAuth::AccessToken.new(consumer, @auth_token, @auth_secret)
35
+ end
36
+
37
+ def authorize_from_access(atoken, asecret)
38
+ @auth_token, @auth_secret = atoken, asecret
39
+ end
40
+
41
+ private
42
+
43
+ # since LinkedIn uses api.linkedin.com for request and access token exchanges,
44
+ # but www.linkedin.com for authorize/authenticate, we have to take care
45
+ # of the url creation ourselves.
46
+ def parse_oauth_options
47
+ {
48
+ :request_token_url => full_oauth_url_for(:request_token, :api_host),
49
+ :access_token_url => full_oauth_url_for(:access_token, :api_host),
50
+ :authorize_url => full_oauth_url_for(:authorize, :auth_host),
51
+ :site => @consumer_options[:site] || @consumer_options[:api_host] || DEFAULT_OAUTH_OPTIONS[:api_host]
52
+ }
53
+ end
54
+
55
+ def full_oauth_url_for(url_type, host_type)
56
+ if @consumer_options["#{url_type}_url".to_sym]
57
+ @consumer_options["#{url_type}_url".to_sym]
58
+ else
59
+ host = @consumer_options[:site] || @consumer_options[host_type] || DEFAULT_OAUTH_OPTIONS[host_type]
60
+ path = @consumer_options[:"#{url_type}_path".to_sym] || DEFAULT_OAUTH_OPTIONS["#{url_type}_path".to_sym]
61
+ "#{host}#{path}"
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,80 @@
1
+ module LinkedIn
2
+ module Helpers
3
+
4
+ module Request
5
+
6
+ DEFAULT_HEADERS = {
7
+ 'x-li-format' => 'json'
8
+ }
9
+
10
+ API_PATH = '/v1'
11
+
12
+ protected
13
+
14
+ def get(path, options={})
15
+ response = access_token.get("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
16
+ raise_errors(response)
17
+ response.body
18
+ end
19
+
20
+ def post(path, body='', options={})
21
+ response = access_token.post("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
22
+ raise_errors(response)
23
+ response
24
+ end
25
+
26
+ def put(path, body, options={})
27
+ response = access_token.put("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
28
+ raise_errors(response)
29
+ response
30
+ end
31
+
32
+ def delete(path, options={})
33
+ response = access_token.delete("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
34
+ raise_errors(response)
35
+ response
36
+ end
37
+
38
+ private
39
+
40
+ def raise_errors(response)
41
+ # Even if the json answer contains the HTTP status code, LinkedIn also sets this code
42
+ # in the HTTP answer (thankfully).
43
+ case response.code.to_i
44
+ when 401
45
+ data = Mash.from_json(response.body)
46
+ raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}"
47
+ when 400
48
+ data = Mash.from_json(response.body)
49
+ raise LinkedIn::Errors::GeneralError.new(data), "(#{data.status}): #{data.message}"
50
+ when 403
51
+ data = Mash.from_json(response.body)
52
+ raise LinkedIn::Errors::AccessDeniedError.new(data), "(#{data.status}): #{data.message}"
53
+ when 404
54
+ raise LinkedIn::Errors::NotFoundError, "(#{response.code}): #{response.message}"
55
+ when 500
56
+ raise LinkedIn::Errors::InformLinkedInError, "LinkedIn had an internal error. Please let them know in the forum. (#{response.code}): #{response.message}"
57
+ when 502..503
58
+ raise LinkedIn::Errors::UnavailableError, "(#{response.code}): #{response.message}"
59
+ end
60
+ end
61
+
62
+ def to_query(options)
63
+ options.inject([]) do |collection, opt|
64
+ collection << "#{opt[0]}=#{opt[1]}"
65
+ collection
66
+ end * '&'
67
+ end
68
+
69
+ def to_uri(path, options)
70
+ uri = URI.parse(path)
71
+
72
+ if options && options != {}
73
+ uri.query = to_query(options)
74
+ end
75
+ uri.to_s
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,68 @@
1
+ require 'hashie'
2
+ require 'multi_json'
3
+
4
+ module LinkedIn
5
+ class Mash < ::Hashie::Mash
6
+
7
+ # a simple helper to convert a json string to a Mash
8
+ def self.from_json(json_string)
9
+ result_hash = ::MultiJson.decode(json_string)
10
+ new(result_hash)
11
+ end
12
+
13
+ # returns a Date if we have year, month and day, and no conflicting key
14
+ def to_date
15
+ if !self.has_key?('to_date') && contains_date_fields?
16
+ Date.civil(self.year, self.month, self.day)
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def timestamp
23
+ value = self['timestamp']
24
+ if value.kind_of? Integer
25
+ value = value / 1000 if value > 9999999999
26
+ Time.at(value)
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ def contains_date_fields?
35
+ self.year? && self.month? && self.day?
36
+ end
37
+
38
+ # overload the convert_key mash method so that the LinkedIn
39
+ # keys are made a little more ruby-ish
40
+ def convert_key(key)
41
+ case key.to_s
42
+ when '_key'
43
+ 'id'
44
+ when '_total'
45
+ 'total'
46
+ when 'values'
47
+ 'all'
48
+ when 'numResults'
49
+ 'total_results'
50
+ else
51
+ underscore(key)
52
+ end
53
+ end
54
+
55
+ # borrowed from ActiveSupport
56
+ # no need require an entire lib when we only need one method
57
+ def underscore(camel_cased_word)
58
+ word = camel_cased_word.to_s.dup
59
+ word.gsub!(/::/, '/')
60
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
61
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
62
+ word.tr!("-", "_")
63
+ word.downcase!
64
+ word
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,56 @@
1
+ module LinkedIn
2
+
3
+ module Search
4
+ def search(options={}, type='people')
5
+
6
+ path = "/#{type.to_s}-search"
7
+
8
+ if options.is_a?(Hash)
9
+ fields = options.delete(:fields)
10
+ path += field_selector(fields) if fields
11
+ end
12
+
13
+ options = { :keywords => options } if options.is_a?(String)
14
+ options = format_options_for_query(options)
15
+
16
+ result_json = get(to_uri(path, options))
17
+
18
+ Mash.from_json(result_json)
19
+ end
20
+
21
+ private
22
+
23
+ def format_options_for_query(opts)
24
+ opts.inject({}) do |list, kv|
25
+ key, value = kv.first.to_s.gsub("_","-"), kv.last
26
+ list[key] = sanitize_value(value)
27
+ list
28
+ end
29
+ end
30
+
31
+ def sanitize_value(value)
32
+ value = value.join("+") if value.is_a?(Array)
33
+ value = value.gsub(" ", "+") if value.is_a?(String)
34
+ value
35
+ end
36
+
37
+ def field_selector(fields)
38
+ result = ":("
39
+ fields = fields.to_a.map do |field|
40
+ if field.is_a?(Hash)
41
+ innerFields = []
42
+ field.each do |key, value|
43
+ innerFields << key.to_s.gsub("_","-") + field_selector(value)
44
+ end
45
+ innerFields.join(',')
46
+ else
47
+ field.to_s.gsub("_","-")
48
+ end
49
+ end
50
+ result += fields.join(',')
51
+ result += ")"
52
+ result
53
+ end
54
+ end
55
+
56
+ end
@@ -0,0 +1,11 @@
1
+ module LinkedIn
2
+
3
+ module VERSION #:nodoc:
4
+ MAJOR = 0
5
+ MINOR = 3
6
+ PATCH = 6
7
+ PRE = nil
8
+ STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
9
+ end
10
+
11
+ end
@@ -0,0 +1,32 @@
1
+ require 'oauth'
2
+
3
+ module LinkedIn
4
+
5
+ class << self
6
+ attr_accessor :token, :secret, :default_profile_fields
7
+
8
+ # config/initializers/linkedin.rb (for instance)
9
+ #
10
+ # LinkedIn.configure do |config|
11
+ # config.token = 'consumer_token'
12
+ # config.secret = 'consumer_secret'
13
+ # config.default_profile_fields = ['education', 'positions']
14
+ # end
15
+ #
16
+ # elsewhere
17
+ #
18
+ # client = LinkedIn::Client.new
19
+ def configure
20
+ yield self
21
+ true
22
+ end
23
+ end
24
+
25
+ autoload :Api, "linked_in/api"
26
+ autoload :Client, "linked_in/client"
27
+ autoload :Mash, "linked_in/mash"
28
+ autoload :Errors, "linked_in/errors"
29
+ autoload :Helpers, "linked_in/helpers"
30
+ autoload :Search, "linked_in/search"
31
+ autoload :Version, "linked_in/version"
32
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../lib/linked_in/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.add_dependency 'hashie', '~> 1.2.0'
6
+ gem.add_dependency 'multi_json', '~> 1.0.3'
7
+ gem.add_dependency 'oauth', '~> 0.4.5'
8
+ gem.add_development_dependency 'json', '~> 1.6'
9
+ gem.add_development_dependency 'rake', '~> 0.9'
10
+ gem.add_development_dependency 'rdoc', '~> 3.8'
11
+ gem.add_development_dependency 'rspec', '~> 2.6'
12
+ gem.add_development_dependency 'simplecov', '~> 0.5'
13
+ gem.add_development_dependency 'vcr', '~> 1.10'
14
+ gem.add_development_dependency 'webmock', '~> 1.7'
15
+ gem.authors = ["Wynn Netherland", "Josh Kalderimis"]
16
+ gem.description = %q{Ruby wrapper for the LinkedIn API}
17
+ gem.email = ['wynn.netherland@gmail.com', 'josh.kalderimis@gmail.com']
18
+ gem.files = `git ls-files`.split("\n")
19
+ gem.homepage = 'http://github.com/pengwynn/linkedin'
20
+ gem.name = 'linkedin-drspin'
21
+ gem.require_paths = ['lib']
22
+ gem.summary = gem.description
23
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ gem.version = LinkedIn::VERSION::STRING
25
+ end
@@ -0,0 +1,92 @@
1
+ require 'helper'
2
+
3
+ describe LinkedIn::Api do
4
+ before do
5
+ LinkedIn.default_profile_fields = nil
6
+ client.stub(:consumer).and_return(consumer)
7
+ client.authorize_from_access('atoken', 'asecret')
8
+ end
9
+
10
+ let(:client){LinkedIn::Client.new('token', 'secret')}
11
+ let(:consumer){OAuth::Consumer.new('token', 'secret', {:site => 'https://api.linkedin.com'})}
12
+
13
+ it "should be able to view the account profile" do
14
+ stub_request(:get, "https://api.linkedin.com/v1/people/~").to_return(:body => "{}")
15
+ client.profile.should be_an_instance_of(LinkedIn::Mash)
16
+ end
17
+
18
+ it "should be able to view public profiles" do
19
+ stub_request(:get, "https://api.linkedin.com/v1/people/id=123").to_return(:body => "{}")
20
+ client.profile(:id => 123).should be_an_instance_of(LinkedIn::Mash)
21
+ end
22
+
23
+ it "should be able to view connections" do
24
+ stub_request(:get, "https://api.linkedin.com/v1/people/~/connections").to_return(:body => "{}")
25
+ client.connections.should be_an_instance_of(LinkedIn::Mash)
26
+ end
27
+
28
+ it "should be able to view network_updates" do
29
+ stub_request(:get, "https://api.linkedin.com/v1/people/~/network/updates").to_return(:body => "{}")
30
+ client.network_updates.should be_an_instance_of(LinkedIn::Mash)
31
+ end
32
+
33
+ it "should be able to search with a keyword if given a String" do
34
+ stub_request(:get, "https://api.linkedin.com/v1/people-search?keywords=business").to_return(:body => "{}")
35
+ client.search("business").should be_an_instance_of(LinkedIn::Mash)
36
+ end
37
+
38
+ it "should be able to search with an option" do
39
+ stub_request(:get, "https://api.linkedin.com/v1/people-search?first-name=Javan").to_return(:body => "{}")
40
+ client.search(:first_name => "Javan").should be_an_instance_of(LinkedIn::Mash)
41
+ end
42
+
43
+ it "should be able to search with an option and fetch specific fields" do
44
+ stub_request(:get, "https://api.linkedin.com/v1/people-search:(num-results,total)?first-name=Javan").to_return(:body => "{}")
45
+ client.search(:first_name => "Javan", :fields => ["num_results", "total"]).should be_an_instance_of(LinkedIn::Mash)
46
+ end
47
+
48
+ it "should be able to share a new status" do
49
+ stub_request(:post, "https://api.linkedin.com/v1/people/~/shares").to_return(:body => "", :status => 201)
50
+ response = client.add_share(:comment => "Testing, 1, 2, 3")
51
+ response.body.should == ""
52
+ response.code.should == "201"
53
+ end
54
+
55
+ context "Company API" do
56
+ use_vcr_cassette
57
+
58
+ it "should be able to view a company profile" do
59
+ stub_request(:get, "https://api.linkedin.com/v1/companies/id=1586").to_return(:body => "{}")
60
+ client.company(:id => 1586).should be_an_instance_of(LinkedIn::Mash)
61
+ end
62
+
63
+ it "should be able to view a company by universal name" do
64
+ stub_request(:get, "https://api.linkedin.com/v1/companies/universal-name=acme").to_return(:body => "{}")
65
+ client.company(:name => 'acme').should be_an_instance_of(LinkedIn::Mash)
66
+ end
67
+
68
+ it "should be able to view a company by e-mail domain" do
69
+ stub_request(:get, "https://api.linkedin.com/v1/companies/email-domain=acme.com").to_return(:body => "{}")
70
+ client.company(:domain => 'acme.com').should be_an_instance_of(LinkedIn::Mash)
71
+ end
72
+
73
+ it "should load correct company data" do
74
+ client.company(:id => 1586).name.should == "Amazon"
75
+
76
+ data = client.company(:id => 1586, :fields => %w{ id name industry locations:(address:(city state country-code) is-headquarters) employee-count-range })
77
+ data.id.should == 1586
78
+ data.name.should == "Amazon"
79
+ data.employee_count_range.name.should == "10001+"
80
+ data.industry.should == "Internet"
81
+ data.locations.all[0].address.city.should == "Seattle"
82
+ data.locations.all[0].is_headquarters.should == true
83
+ end
84
+ end
85
+
86
+ context "Errors" do
87
+ it "should raise AccessDeniedError when LinkedIn returns 403 status code" do
88
+ stub_request(:get, "https://api.linkedin.com/v1/people-search?first-name=Javan").to_return(:body => "{}", :status => 403)
89
+ expect{ client.search(:first_name => "Javan") }.to raise_error(LinkedIn::Errors::AccessDeniedError)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,37 @@
1
+ require 'helper'
2
+
3
+ describe LinkedIn do
4
+
5
+ before(:each) do
6
+ LinkedIn.token = nil
7
+ LinkedIn.secret = nil
8
+ LinkedIn.default_profile_fields = nil
9
+ end
10
+
11
+ it "should be able to set the consumer token and consumer secret" do
12
+ LinkedIn.token = 'consumer_token'
13
+ LinkedIn.secret = 'consumer_secret'
14
+
15
+ LinkedIn.token.should == 'consumer_token'
16
+ LinkedIn.secret.should == 'consumer_secret'
17
+ end
18
+
19
+ it "should be able to set the default profile fields" do
20
+ LinkedIn.default_profile_fields = ['education', 'positions']
21
+
22
+ LinkedIn.default_profile_fields.should == ['education', 'positions']
23
+ end
24
+
25
+ it "should be able to set the consumer token and consumer secret via a configure block" do
26
+ LinkedIn.configure do |config|
27
+ config.token = 'consumer_token'
28
+ config.secret = 'consumer_secret'
29
+ config.default_profile_fields = ['education', 'positions']
30
+ end
31
+
32
+ LinkedIn.token.should == 'consumer_token'
33
+ LinkedIn.secret.should == 'consumer_secret'
34
+ LinkedIn.default_profile_fields.should == ['education', 'positions']
35
+ end
36
+
37
+ end