wordnik 4.07 → 4.08

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