crankin 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) 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 +82 -0
  16. data/examples/status.rb +9 -0
  17. data/lib/linked_in/api/query_methods.rb +101 -0
  18. data/lib/linked_in/api/update_methods.rb +82 -0
  19. data/lib/linked_in/api.rb +6 -0
  20. data/lib/linked_in/client.rb +46 -0
  21. data/lib/linked_in/errors.rb +18 -0
  22. data/lib/linked_in/helpers/authorization.rb +68 -0
  23. data/lib/linked_in/helpers/request.rb +78 -0
  24. data/lib/linked_in/helpers.rb +6 -0
  25. data/lib/linked_in/mash.rb +68 -0
  26. data/lib/linked_in/search.rb +56 -0
  27. data/lib/linked_in/version.rb +11 -0
  28. data/lib/linkedin.rb +32 -0
  29. data/linkedin.gemspec +25 -0
  30. data/spec/cases/api_spec.rb +86 -0
  31. data/spec/cases/linkedin_spec.rb +37 -0
  32. data/spec/cases/mash_spec.rb +85 -0
  33. data/spec/cases/oauth_spec.rb +166 -0
  34. data/spec/cases/search_spec.rb +127 -0
  35. data/spec/fixtures/cassette_library/LinkedIn_Api/Company_API.yml +73 -0
  36. data/spec/fixtures/cassette_library/LinkedIn_Client/_authorize_from_request.yml +28 -0
  37. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_a_callback_url.yml +28 -0
  38. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token/with_default_options.yml +28 -0
  39. data/spec/fixtures/cassette_library/LinkedIn_Client/_request_token.yml +28 -0
  40. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_company_name_option.yml +92 -0
  41. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options.yml +43 -0
  42. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_first_name_and_last_name_options_with_fields.yml +45 -0
  43. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_keywords_string_parameter.yml +92 -0
  44. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option.yml +92 -0
  45. data/spec/fixtures/cassette_library/LinkedIn_Search/_search/by_single_keywords_option_with_pagination.yml +67 -0
  46. data/spec/helper.rb +30 -0
  47. metadata +205 -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,78 @@
1
+ module LinkedIn
2
+ module Helpers
3
+
4
+ module Request
5
+
6
+ DEFAULT_HEADERS = {
7
+ 'x-li-format' => 'json'
8
+ }
9
+
10
+ # thanks to @coderifous
11
+ API_PATH = 'https://api.linkedin.com/v1'
12
+
13
+ protected
14
+
15
+ def get(path, options={})
16
+ response = access_token.get("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
17
+ raise_errors(response)
18
+ response.body
19
+ end
20
+
21
+ def post(path, body='', options={})
22
+ response = access_token.post("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
23
+ raise_errors(response)
24
+ response
25
+ end
26
+
27
+ def put(path, body, options={})
28
+ response = access_token.put("#{API_PATH}#{path}", body, DEFAULT_HEADERS.merge(options))
29
+ raise_errors(response)
30
+ response
31
+ end
32
+
33
+ def delete(path, options={})
34
+ response = access_token.delete("#{API_PATH}#{path}", DEFAULT_HEADERS.merge(options))
35
+ raise_errors(response)
36
+ response
37
+ end
38
+
39
+ private
40
+
41
+ def raise_errors(response)
42
+ # Even if the json answer contains the HTTP status code, LinkedIn also sets this code
43
+ # in the HTTP answer (thankfully).
44
+ case response.code.to_i
45
+ when 401
46
+ data = Mash.from_json(response.body)
47
+ raise LinkedIn::Errors::UnauthorizedError.new(data), "(#{data.status}): #{data.message}"
48
+ when 400, 403
49
+ data = Mash.from_json(response.body)
50
+ raise LinkedIn::Errors::GeneralError.new(data), "(#{data.status}): #{data.message}"
51
+ when 404
52
+ raise LinkedIn::Errors::NotFoundError, "(#{response.code}): #{response.message}"
53
+ when 500
54
+ raise LinkedIn::Errors::InformLinkedInError, "LinkedIn had an internal error. Please let them know in the forum. (#{response.code}): #{response.message}"
55
+ when 502..503
56
+ raise LinkedIn::Errors::UnavailableError, "(#{response.code}): #{response.message}"
57
+ end
58
+ end
59
+
60
+ def to_query(options)
61
+ options.inject([]) do |collection, opt|
62
+ collection << "#{opt[0]}=#{opt[1]}"
63
+ collection
64
+ end * '&'
65
+ end
66
+
67
+ def to_uri(path, options)
68
+ uri = URI.parse(path)
69
+
70
+ if options && options != {}
71
+ uri.query = to_query(options)
72
+ end
73
+ uri.to_s
74
+ end
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,6 @@
1
+ module LinkedIn
2
+ module Helpers
3
+ autoload :Authorization, "linked_in/helpers/authorization"
4
+ autoload :Request, "linked_in/helpers/request"
5
+ end
6
+ 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
+
5
+ def search(options={})
6
+ path = "/people-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
data/lib/linkedin.rb ADDED
@@ -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
data/linkedin.gemspec ADDED
@@ -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", "Matt Burke", "Ben Shymkiw"]
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/iamsolarpowered/linkedin'
20
+ gem.name = 'crankin'
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,86 @@
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
+ 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
@@ -0,0 +1,85 @@
1
+ require 'helper'
2
+
3
+ describe LinkedIn::Mash do
4
+
5
+ describe ".from_json" do
6
+ it "should convert a json string to a Mash" do
7
+ json_string = "{\"name\":\"Josh Kalderimis\"}"
8
+ mash = LinkedIn::Mash.from_json(json_string)
9
+
10
+ mash.should have_key('name')
11
+ mash.name.should == 'Josh Kalderimis'
12
+ end
13
+ end
14
+
15
+ describe "#convert_keys" do
16
+ let(:mash) do
17
+ LinkedIn::Mash.new({
18
+ 'firstName' => 'Josh',
19
+ 'LastName' => 'Kalderimis',
20
+ '_key' => 1234,
21
+ '_total' => 1234,
22
+ 'values' => {},
23
+ 'numResults' => 'total_results'
24
+ })
25
+ end
26
+
27
+ it "should convert camal cased hash keys to underscores" do
28
+ mash.should have_key('first_name')
29
+ mash.should have_key('last_name')
30
+ end
31
+
32
+ it "should convert the key _key to id" do
33
+ mash.should have_key('id')
34
+ end
35
+
36
+ it "should convert the key _total to total" do
37
+ mash.should have_key('total')
38
+ end
39
+
40
+ it "should convert the key values to all" do
41
+ mash.should have_key('all')
42
+ end
43
+
44
+ it "should convert the key numResults to total_results" do
45
+ mash.should have_key('total_results')
46
+ end
47
+ end
48
+
49
+ describe '#timestamp' do
50
+ it "should return a valid Time if a key of timestamp exists and the value is an int" do
51
+ time_mash = LinkedIn::Mash.new({ 'timestamp' => 1297083249 })
52
+
53
+ time_mash.timestamp.should be_a_kind_of(Time)
54
+ time_mash.timestamp.to_i.should == 1297083249
55
+ end
56
+
57
+ it "should return a valid Time if a key of timestamp exists and the value is an int which is greater than 9999999999" do
58
+ time_mash = LinkedIn::Mash.new({ 'timestamp' => 1297083249 * 1000 })
59
+
60
+ time_mash.timestamp.should be_a_kind_of(Time)
61
+ time_mash.timestamp.to_i.should == 1297083249
62
+ end
63
+
64
+ it "should not try to convert to a Time object if the value isn't an Integer" do
65
+ time_mash = LinkedIn::Mash.new({ 'timestamp' => 'Foo' })
66
+
67
+ time_mash.timestamp.class.should be String
68
+ end
69
+ end
70
+
71
+ describe "#to_date" do
72
+ let(:date_mash) do
73
+ LinkedIn::Mash.new({
74
+ 'year' => 2010,
75
+ 'month' => 06,
76
+ 'day' => 23
77
+ })
78
+ end
79
+
80
+ it "should return a valid Date if the keys year, month, day all exist" do
81
+ date_mash.to_date.should == Date.civil(2010, 06, 23)
82
+ end
83
+ end
84
+
85
+ end