wordnik 4.07 → 4.08

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wordnik (4.06.15)
4
+ wordnik (4.08)
5
5
  activemodel (>= 3.0.3)
6
6
  addressable (>= 2.2.4)
7
7
  htmlentities (>= 4.2.4)
@@ -13,25 +13,26 @@ GEM
13
13
  remote: http://rubygems.org/
14
14
  specs:
15
15
  ZenTest (4.6.2)
16
- activemodel (3.2.1)
17
- activesupport (= 3.2.1)
16
+ activemodel (3.2.7)
17
+ activesupport (= 3.2.7)
18
18
  builder (~> 3.0.0)
19
- activesupport (3.2.1)
19
+ activesupport (3.2.7)
20
20
  i18n (~> 0.6)
21
21
  multi_json (~> 1.0)
22
22
  addressable (2.2.6)
23
23
  autotest (4.4.6)
24
24
  ZenTest (>= 4.4.1)
25
25
  autotest-rails-pure (4.1.2)
26
- builder (3.0.0)
26
+ builder (3.0.3)
27
27
  crack (0.1.8)
28
28
  diff-lcs (1.1.3)
29
29
  htmlentities (4.3.1)
30
- i18n (0.6.0)
31
- json (1.6.5)
32
- mime-types (1.17.2)
33
- multi_json (1.1.0)
34
- nokogiri (1.5.0)
30
+ i18n (0.6.1)
31
+ json (1.7.5)
32
+ mime-types (1.19)
33
+ multi_json (1.3.6)
34
+ nokogiri (1.5.5)
35
+ rake (0.9.2.2)
35
36
  rspec (2.8.0)
36
37
  rspec-core (~> 2.8.0)
37
38
  rspec-expectations (~> 2.8.0)
@@ -40,6 +41,7 @@ GEM
40
41
  rspec-expectations (2.8.0)
41
42
  diff-lcs (~> 1.1.2)
42
43
  rspec-mocks (2.8.0)
44
+ ruby-prof (0.11.2)
43
45
  typhoeus (0.3.3)
44
46
  mime-types
45
47
  vcr (1.11.3)
@@ -53,7 +55,9 @@ PLATFORMS
53
55
  DEPENDENCIES
54
56
  autotest
55
57
  autotest-rails-pure
58
+ rake
56
59
  rspec (~> 2.8.0)
60
+ ruby-prof
57
61
  vcr (~> 1.11.3)
58
62
  webmock (>= 1.6.2)
59
63
  wordnik!
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  wordnik rubygem
2
2
  ===============
3
3
 
4
- This is the official Wordnik rubygem. It fully wraps Wordnik's v4 API. Refer to
5
- [developer.wordnik.com/docs](http://developer.wordnik.com/docs) to play around
6
- in the live API sandbox. All the methods you see there are implemented in this
4
+ This is the official Wordnik rubygem. It fully wraps Wordnik's v4 API. Refer to
5
+ [developer.wordnik.com/docs](http://developer.wordnik.com/docs) to play around
6
+ in the live API sandbox. All the methods you see there are implemented in this
7
7
  ruby gem.
8
8
 
9
9
  Installation
@@ -69,7 +69,7 @@ Wordnik.words.search_words(:query => '*tin*', :include_part_of_speech => 'verb',
69
69
  ```
70
70
 
71
71
  For a full list of available methods, check out the [Wordnik API documentation](http://developer.wordnik.com/docs).
72
- When you make a request using our web-based API sandbox, the response output will show you how to make the
72
+ When you make a request using our web-based API sandbox, the response output will show you how to make the
73
73
  [equivalent ruby request](http://cl.ly/9FQY). w00t!
74
74
 
75
75
  Specs
@@ -77,14 +77,7 @@ Specs
77
77
 
78
78
  The wordnik gem uses rspec 2. To run the test suite, just type `rake` or `bundle exec rake spec` in the gem's base directory.
79
79
 
80
- Note
81
- ----
82
- For testing locally, you will need to tunnel into the beta box
83
80
 
84
- ssh -f -N -L 8001:localhost:8001 beta.wordnik.com
85
-
86
- And remember to update the spec_helper
87
-
88
81
  Contributing
89
82
  ------------
90
83
 
@@ -95,16 +88,29 @@ Contributing
95
88
  * Commit and push until you are happy with your contribution
96
89
  * Make sure to add tests for the feature/bugfix. This is important so we don't break it in a future version unintentionally.
97
90
 
98
- Wishlist
99
- --------
91
+ Releasing
92
+ ---------
100
93
 
101
- * Go Kart
102
- * Helicopter
94
+ ```bash
95
+ rake swagger
96
+ open lib/version.rb # bump the version number
97
+ rake spec # test
98
+ git commit -am "newness" # commit
99
+ git push origin master # push
100
+ rake release # release
101
+ ```
103
102
 
104
103
  Props
105
104
  -----
106
105
 
107
- * Thanks to [Jason Adams](http://twitter.com/#!/ealdent) for graciously turning
106
+ * Thanks to [Jason Adams](http://twitter.com/#!/ealdent) for graciously turning
108
107
  over the [wordnik gem name](https://rubygems.org/gems/wordnik).
109
- * HTTP requests are made using [Typhoeus](https://github.com/dbalatero/typhoeus),
110
- a modern code version of the mythical beast with 100 serpent heads.
108
+ * HTTP requests are made using [Typhoeus](https://github.com/dbalatero/typhoeus),
109
+ a modern code version of the mythical beast with 100 serpent heads.
110
+
111
+ Notes
112
+ -----
113
+
114
+ * If you are using the Wordnik gem on [Heroku](http://www.heroku.com/), you'll need
115
+ to use a stack that is compatible with [Typhoeus](https://github.com/dbalatero/typhoeus).
116
+ As of 2012-08, this means the Cedar stack.
@@ -6,6 +6,7 @@ require 'wordnik/resource'
6
6
  require 'wordnik/response'
7
7
  require 'wordnik/configuration'
8
8
  require 'wordnik/version'
9
+ require 'wordnik/load_balancer'
9
10
  require 'logger'
10
11
 
11
12
  # http://blog.jayfields.com/2007/10/ruby-defining-class-methods.html
@@ -16,17 +17,17 @@ class Object
16
17
  end
17
18
 
18
19
  module Wordnik
19
-
20
+
20
21
  class << self
21
-
22
+
22
23
  # A Wordnik configuration object. Must act like a hash and return sensible
23
24
  # values for all Wordnik configuration options. See Wordnik::Configuration.
24
25
  attr_accessor :configuration
25
26
 
26
27
  attr_accessor :resources
27
-
28
+
28
29
  attr_accessor :logger
29
-
30
+
30
31
  # Call this method to modify defaults in your initializers.
31
32
  #
32
33
  # @example
@@ -43,7 +44,7 @@ module Wordnik
43
44
 
44
45
  # Configure logger. Default to use Rails
45
46
  self.logger ||= configuration.logger || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT))
46
-
47
+
47
48
  # remove :// from scheme
48
49
  configuration.scheme.sub!(/:\/\//, '')
49
50
 
@@ -51,6 +52,15 @@ module Wordnik
51
52
  configuration.host.sub!(/https?:\/\//, '')
52
53
  configuration.host = configuration.host.split('/').first
53
54
 
55
+ # do the same if multiple hosts are specified
56
+ configuration.hosts = configuration.hosts.map{|host| host.sub(/https?:\/\//, '').split('/').first}
57
+
58
+ # create a load balancer if no load balancer specified && multiple hosts are specified ...
59
+ if !configuration.load_balancer && configuration.hosts.size > 0
60
+ self.logger.debug "Creating a load balancer from #{configuration.hosts.join(', ')}"
61
+ configuration.load_balancer = LoadBalancer.new(configuration.hosts)
62
+ end
63
+
54
64
  # Add leading and trailing slashes to base_path
55
65
  configuration.base_path = "/#{configuration.base_path}".gsub(/\/+/, '/')
56
66
  configuration.base_path = "" if configuration.base_path == "/"
@@ -59,17 +69,17 @@ module Wordnik
59
69
  # attach resources because they haven't been downloaded.
60
70
  if build
61
71
  self.instantiate_resources
62
- self.create_resource_shortcuts
72
+ self.create_resource_shortcuts
63
73
  end
64
74
  end
65
-
75
+
66
76
  # Remove old JSON documentation and generated modules,
67
77
  # then download fresh JSON files.
68
78
  #
69
79
  def download_resource_descriptions
70
80
  system "rm api_docs/*.json"
71
81
  system "rm lib/wordnik/resource_modules/*.rb"
72
-
82
+
73
83
  Wordnik::Request.new(:get, "resources.json").response.body['apis'].each do |api|
74
84
  resource_name = api['path'].split(".").first.gsub(/\//, '')
75
85
  description = api['description']
@@ -84,19 +94,19 @@ module Wordnik
84
94
  # 1. Instantiate a Resource object
85
95
  # 2. Stuff the Resource in Wordnik.resources
86
96
  #
87
- def instantiate_resources
97
+ def instantiate_resources
88
98
  self.resources = {}
89
99
  self.configuration.resource_names.each do |resource_name|
90
100
  name = resource_name.underscore.to_sym # 'fooBar' => :foo_bar
91
- filename = File.join(File.dirname(__FILE__), "../api_docs/#{resource_name}.json")
101
+ filename = File.join(File.dirname(__FILE__), "../api_docs/#{resource_name}.json")
92
102
  resource = Resource.new(
93
103
  :name => name,
94
104
  :raw_data => JSON.parse(File.read(filename))
95
105
  )
96
106
  self.resources[name] = resource
97
- end
107
+ end
98
108
  end
99
-
109
+
100
110
  # Use some magic ruby dust to make nice method shortcuts.
101
111
  # Wordnik.word => Wordnik.resources[:word]
102
112
  # Wordnik.users => Wordnik.resources[:user]
@@ -109,39 +119,43 @@ module Wordnik
109
119
  end
110
120
  end
111
121
  end
112
-
122
+
113
123
  def authenticated?
114
124
  Wordnik.configuration.user_id.present? && Wordnik.configuration.auth_token.present?
115
125
  end
116
-
126
+
117
127
  def de_authenticate
118
128
  Wordnik.configuration.user_id = nil
119
129
  Wordnik.configuration.auth_token = nil
120
130
  end
121
-
131
+
132
+ def clear_configuration
133
+ Wordnik.configuration = Configuration.new
134
+ end
135
+
122
136
  def authenticate
123
137
  return if Wordnik.authenticated?
124
-
138
+
125
139
  if Wordnik.configuration.username.blank? || Wordnik.configuration.password.blank?
126
140
  raise ClientError, "Username and password are required to authenticate."
127
141
  end
128
-
142
+
129
143
  request = Wordnik::Request.new(
130
- :get,
131
- "account/authenticate/{username}",
144
+ :get,
145
+ "account/authenticate/{username}",
132
146
  :params => {
133
- :username => Wordnik.configuration.username,
147
+ :username => Wordnik.configuration.username,
134
148
  :password => Wordnik.configuration.password
135
149
  }
136
150
  )
137
-
151
+
138
152
  response_body = request.response.body
139
153
  Wordnik.configuration.user_id = response_body['userId']
140
154
  Wordnik.configuration.auth_token = response_body['token']
141
155
  end
142
156
 
143
157
  end
144
-
158
+
145
159
  end
146
160
 
147
161
  class ServerError < StandardError
@@ -7,35 +7,39 @@ module Wordnik
7
7
  attr_accessor :api_key
8
8
  attr_accessor :username
9
9
  attr_accessor :password
10
-
10
+
11
11
  # TODO: Steal all the auth stuff from the old gem!
12
12
  attr_accessor :auth_token
13
13
  attr_accessor :user_id
14
-
14
+
15
15
  # Response format can be 'json' (default) or 'xml'
16
16
  attr_accessor :response_format
17
-
17
+
18
18
  # A comma-delimited list of the API's resources
19
19
  attr_accessor :resource_names
20
-
20
+
21
21
  # The URL of the API server
22
22
  attr_accessor :scheme
23
23
  attr_accessor :host
24
+ attr_accessor :hosts # to do in process load balancing
25
+ attr_accessor :load_balancer
24
26
  attr_accessor :base_path
25
-
27
+
26
28
  attr_accessor :user_agent
27
-
29
+
28
30
  attr_accessor :proxy
29
31
  attr_accessor :proxy_username
30
32
  attr_accessor :proxy_password
31
33
 
32
34
  attr_accessor :logger
33
-
35
+
34
36
  # Defaults go in here..
35
37
  def initialize
36
38
  @response_format = 'json'
37
39
  @scheme = 'http'
38
40
  @host = 'api.wordnik.com'
41
+ @hosts = []
42
+ @load_balancer = nil
39
43
  @base_path = '/v4'
40
44
  @user_agent = "ruby-#{Wordnik::VERSION}"
41
45
  # Build the default set of resource names from the filenames of the API documentation
@@ -47,7 +51,7 @@ module Wordnik
47
51
  raise "Problem loading the resource files in ./api_docs/"
48
52
  end
49
53
  end
50
-
54
+
51
55
  def base_url
52
56
  Addressable::URI.new(
53
57
  :scheme => self.scheme,
@@ -56,6 +60,14 @@ module Wordnik
56
60
  )
57
61
  end
58
62
 
63
+ def clear
64
+ initialize
65
+ end
66
+
67
+ def host
68
+ @load_balancer ? @load_balancer.host : @host
69
+ end
70
+
59
71
  end
60
72
 
61
73
  end
@@ -0,0 +1,71 @@
1
+ module Wordnik
2
+
3
+ # the simple idea here of a load balancer is to keep a set of hosts
4
+ # around, and use the 'best' one. At Wordnik, we have a set of
5
+ # API hosts available to us (these servers are invisible to the general web)
6
+ # The class below implements least recently used.
7
+ #
8
+ # The Wordnik Configuration object will convert a :hosts specification
9
+ # into a LoadBalancer instance
10
+ #
11
+
12
+ #
13
+ # These should be thread safe.
14
+ class LoadBalancer
15
+
16
+ attr_reader :hosts
17
+ attr_accessor :all_hosts
18
+ attr_accessor :failed_hosts_table
19
+ attr_accessor :current_host
20
+
21
+ def initialize(hosts)
22
+ @all_hosts = hosts
23
+ @hosts = @all_hosts
24
+ @failed_hosts_table = {}
25
+ @current_host = nil
26
+ end
27
+
28
+ def host
29
+ @current_host = hosts.first
30
+ @hosts.rotate!
31
+ restore_failed_hosts_maybe
32
+ @current_host
33
+ end
34
+
35
+ def inform_failure
36
+ #Wordnik.logger.debug "Informing failure about #{@current_host}. table: #{@failed_hosts_table.inspect}"
37
+ if @failed_hosts_table.include?(@current_host)
38
+ failures, failed_time = @failed_hosts_table[@current_host]
39
+ @failed_hosts_table[@current_host] = [failures+1, failed_time]
40
+ else
41
+ @failed_hosts_table[@current_host] = [1, Time.now.to_f] # failure count, first failure time
42
+ end
43
+ #Wordnik.logger.debug "Informed failure about #{@current_host}. table now: #{@failed_hosts_table.inspect}"
44
+ @hosts.delete(@current_host)
45
+ @hosts = [@current_host] if @hosts.size == 0 # got to have something!
46
+ end
47
+
48
+ # success here means just that a successful connection was made
49
+ # and the website didn't time out.
50
+ def inform_success
51
+ @failed_hosts_table.delete(@current_host)
52
+ @hosts << @current_host unless @hosts.include? @current_host
53
+ @hosts
54
+ end
55
+
56
+ def restore_failed_hosts_maybe
57
+ return if @failed_hosts_table.size == 0
58
+ @failed_hosts_table.each do |host, pair|
59
+ failures, failed_time = pair
60
+ seconds_since_first_failure = (Time.now.to_f - failed_time)
61
+ #Wordnik.logger.debug "Seconds since #{host}'s first failure: #{seconds_since_first_failure} compared to #{2**(failures-1)}"
62
+ # exponential backoff, but try every hour...
63
+ if (seconds_since_first_failure > [3600, 2**(failures-1)].min)
64
+ @hosts << host # give it a chance to succeed ...
65
+ #Wordnik.logger.debug "Added #{host} to @hosts; now: #{@hosts}"
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ end
@@ -16,7 +16,7 @@ module Wordnik
16
16
 
17
17
  # All requests must have an HTTP method and a path
18
18
  # Optionals parameters are :params, :headers, :body, :format, :host
19
- #
19
+ #
20
20
  def initialize(http_method, path, attributes={})
21
21
  attributes[:format] ||= Wordnik.configuration.response_format
22
22
  attributes[:params] ||= {}
@@ -32,21 +32,21 @@ module Wordnik
32
32
  if attributes[:headers].present? && attributes[:headers].has_key?(:api_key)
33
33
  default_headers.delete(:api_key)
34
34
  end
35
-
35
+
36
36
  # api_key from params hash trumps all others (headers and default_headers)
37
37
  if attributes[:params].present? && attributes[:params].has_key?(:api_key)
38
38
  default_headers.delete(:api_key)
39
39
  attributes[:headers].delete(:api_key) if attributes[:headers].present?
40
40
  end
41
-
41
+
42
42
  # Merge argument headers into defaults
43
43
  attributes[:headers] = default_headers.merge(attributes[:headers] || {})
44
-
44
+
45
45
  # Stick in the auth token if there is one
46
46
  if Wordnik.authenticated?
47
47
  attributes[:headers].merge!({:auth_token => Wordnik.configuration.auth_token})
48
48
  end
49
-
49
+
50
50
  self.http_method = http_method.to_sym
51
51
  self.path = path
52
52
  attributes.each do |name, value|
@@ -56,20 +56,20 @@ module Wordnik
56
56
 
57
57
  # Construct a base URL
58
58
  #
59
- def url(options = {})
59
+ def url(options = {})
60
60
  u = Addressable::URI.new(
61
61
  :scheme => Wordnik.configuration.scheme,
62
62
  :host => Wordnik.configuration.host,
63
63
  :path => self.interpreted_path,
64
64
  :query => self.query_string.sub(/\?/, '')
65
65
  ).to_s
66
-
66
+
67
67
  # Drop trailing question mark, if present
68
68
  u.sub! /\?$/, ''
69
-
69
+
70
70
  # Obfuscate API key?
71
71
  u.sub! /api\_key=\w+/, 'api_key=YOUR_API_KEY' if options[:obfuscated]
72
-
72
+
73
73
  u
74
74
  end
75
75
 
@@ -91,14 +91,14 @@ module Wordnik
91
91
  end
92
92
 
93
93
  p = p.sub("{format}", self.format.to_s)
94
-
94
+
95
95
  URI.encode [Wordnik.configuration.base_path, p].join("/").gsub(/\/+/, '/')
96
96
  end
97
-
97
+
98
98
  # Massage the request body into a state of readiness
99
99
  # If body is a hash, camelize all keys then convert to a json string
100
100
  #
101
- def body=(value)
101
+ def body=(value)
102
102
  if value.is_a?(Hash)
103
103
  value = value.inject({}) do |memo, (k,v)|
104
104
  memo[k.to_s.camelize(:lower).to_sym] = v
@@ -107,13 +107,13 @@ module Wordnik
107
107
  end
108
108
  @body = value
109
109
  end
110
-
110
+
111
111
  # If body is an object, JSONify it before making the actual request.
112
- #
112
+ #
113
113
  def outgoing_body
114
114
  body.is_a?(String) ? body : body.to_json
115
115
  end
116
-
116
+
117
117
  # Construct a query string from the query-string-type params
118
118
  def query_string
119
119
 
@@ -127,48 +127,69 @@ module Wordnik
127
127
  key = key.to_s.camelize(:lower).to_sym unless key.to_sym == :api_key # api_key is not a camelCased param
128
128
  query_values[key] = value.to_s
129
129
  end
130
-
130
+
131
131
  # We don't want to end up with '?' as our query string
132
132
  # if there aren't really any params
133
133
  return "" if query_values.blank?
134
-
134
+
135
135
  # Addressable requires query_values to be set after initialization..
136
136
  qs = Addressable::URI.new
137
137
  qs.query_values = query_values
138
138
  qs.to_s
139
139
  end
140
-
141
- def make
142
- request = Typhoeus::Request.new(self.url,
140
+
141
+ def make(attempt = 0)
142
+ # url is calculated, so we need to compute it once.
143
+ u = self.url
144
+ #Wordnik.logger.debug "Making attempt #{attempt}; now fetching #{u}" if attempt > 0
145
+ request = Typhoeus::Request.new(u,
143
146
  :headers => self.headers.stringify_keys,
144
147
  :method => self.http_method.to_sym)
145
-
146
- # Make request proxy-aware
148
+
149
+ # Make request proxy-aware
147
150
  if Wordnik.configuration.proxy.present?
148
151
  request.proxy = Wordnik.configuration.proxy
149
152
  request.proxy_username = Wordnik.configuration.proxy_username if Wordnik.configuration.proxy_username.present?
150
153
  request.proxy_password = Wordnik.configuration.proxy_password if Wordnik.configuration.proxy_password.present?
151
154
  end
152
-
153
- Wordnik.logger.debug "\n #{self.http_method.to_s.upcase} #{self.url}\n body: #{self.outgoing_body}\n headers: #{request.headers}\n\n"
154
-
155
+
156
+ Wordnik.logger.debug "\n #{self.http_method.to_s.upcase} #{u}\n body: #{self.outgoing_body}\n headers: #{request.headers}\n\n"
157
+
155
158
  request.body = self.outgoing_body unless self.http_method.to_sym == :get
156
159
 
157
- # Execute the request
160
+ # Execute the request — blocking call here.
158
161
  Typhoeus::Hydra.hydra.queue request
159
- Typhoeus::Hydra.hydra.run
160
- Response.new(request.response)
162
+ Typhoeus::Hydra.hydra.run
163
+
164
+ # if we are using local load balancing, check for timeouts and connection errors
165
+ resp = request.response
166
+
167
+ if Wordnik.configuration.load_balancer
168
+ if (resp.timed_out? || resp.code == 0)
169
+ # Wordnik.logger.debug "informing load balancer about failure"
170
+ Wordnik.configuration.load_balancer.inform_failure
171
+ if (attempt <= 3)
172
+ # Wordnik.logger.debug "Trying again after failing #{attempt} times..."
173
+ return make(attempt + 1) if attempt <= 3 # try three times to get a result...
174
+ end
175
+ else
176
+ # Wordnik.logger.debug "informing load balancer about success"
177
+ Wordnik.configuration.load_balancer.inform_success
178
+ end
179
+ end
180
+
181
+ Response.new(resp)
161
182
  end
162
-
183
+
163
184
  def response
164
185
  self.make
165
186
  end
166
-
187
+
167
188
  def response_code_pretty
168
189
  return unless @response.present?
169
- @response.code.to_s
190
+ @response.code.to_s
170
191
  end
171
-
192
+
172
193
  def response_headers_pretty
173
194
  return unless @response.present?
174
195
  # JSON.pretty_generate(@response.headers).gsub(/\n/, '<br/>') # <- This was for RestClient