rest-core 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ rdoc
4
4
  .bundle
5
5
  .yardoc
6
6
  Gemfile.lock
7
+ task
data/CHANGES.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # CHANGES
2
2
 
3
+ ## rest-core 1.0.2 -- 2012-06-05
4
+
5
+ ### Enhancement
6
+
7
+ * Some internal refactoring.
8
+
9
+ ### Bugs fixes
10
+
11
+ * Properly handle asynchronous timers for eventmachine and cool.io.
12
+
3
13
  ## rest-core 1.0.1 -- 2012-05-14
4
14
 
5
15
  * [`Auto`] Check for eventmachine first instead of cool.io
data/Rakefile CHANGED
@@ -1,8 +1,13 @@
1
1
  # encoding: utf-8
2
2
 
3
- require "#{dir = File.dirname(__FILE__)}/task/gemgem"
4
- Gemgem.dir = dir
3
+ begin
4
+ require "#{dir = File.dirname(__FILE__)}/task/gemgem"
5
+ rescue LoadError
6
+ sh "git submodule update --init"
7
+ exec Gem.ruby, "-S", "rake", *ARGV
8
+ end
5
9
 
10
+ Gemgem.dir = dir
6
11
  ($LOAD_PATH << File.expand_path("#{Gemgem.dir}/lib" )).uniq!
7
12
 
8
13
  desc 'Generate gemspec'
@@ -11,10 +11,10 @@ class RestCore::CoolioFiber
11
11
  :url => request_uri(env) ,
12
12
  :payload => env[REQUEST_PAYLOAD],
13
13
  :headers => env[REQUEST_HEADERS]))
14
+ rescue FiberError
14
15
  end
15
16
 
16
17
  def process env, response
17
- env[TIMER].detach if env[TIMER]
18
18
  Thread.current[:coolio_http_client].detach if
19
19
  Thread.current[:coolio_http_client].kind_of?(::Coolio::HttpFiber)
20
20
 
@@ -28,7 +28,7 @@ class RestCore::EmHttpRequestAsync
28
28
  end
29
29
 
30
30
  def respond env, client
31
- env[TIMER].cancel if env[TIMER]
31
+ env[TIMER].cancel if env[TIMER] && !env[TIMER].canceled?
32
32
  env[ASYNC].call(env.merge(
33
33
  RESPONSE_BODY => client.response,
34
34
  RESPONSE_STATUS => client.response_header.status,
@@ -28,8 +28,11 @@ class RestCore::EmHttpRequestFiber
28
28
  end
29
29
 
30
30
  def respond f, env, client
31
- env[TIMER].cancel if env[TIMER]
32
31
  f.resume(process(env, client)) if f.alive?
32
+ rescue FiberError
33
+ # whenever timeout, client.close would be called,
34
+ # and then errback would be called. in this case,
35
+ # the fiber is already resumed by the timer
33
36
  end
34
37
 
35
38
  def process env, client
@@ -6,9 +6,6 @@ class RestCore::Builder
6
6
  include RestCore
7
7
  include Wrapper
8
8
 
9
- class << self
10
- attr_writer :default_app
11
- end
12
9
  def self.default_app
13
10
  @default_app ||= RestClient
14
11
  end
@@ -176,7 +176,7 @@ module RestCore::Client
176
176
  FAIL => [] ,
177
177
  LOG => [] ,
178
178
  ASYNC => if block_given?
179
- lambda{ |response| yield(response) }
179
+ lambda{ |res| yield(res) }
180
180
  else
181
181
  nil
182
182
  end}.merge(env)))
@@ -23,6 +23,7 @@ class RestCore::Cache
23
23
  end
24
24
 
25
25
  if cached = cache_get(e)
26
+ env[TIMER].cancel if env[TIMER] && !env[TIMER].canceled?
26
27
  wrapped.call(cached)
27
28
  else
28
29
  if e[ASYNC]
@@ -25,7 +25,10 @@ class RestCore::Timeout
25
25
  if root_fiber? && env[ASYNC]
26
26
  yield(env.merge(TIMER => timeout_with_callback(env, class_name)))
27
27
  else
28
- yield(env.merge(TIMER => timeout_with_resume( env, class_name)))
28
+ timer = timeout_with_resume(env, class_name)
29
+ response = yield(env.merge(TIMER => timer))
30
+ timer.cancel unless timer.canceled?
31
+ response
29
32
  end
30
33
  else
31
34
  ::Timeout.timeout(timeout(env)){ yield(env) }
@@ -60,6 +63,9 @@ class RestCore::Timeout
60
63
  f = Fiber.current
61
64
  EventMachineTimer.new(timeout(env), error = timeout_error){
62
65
  f.resume(error) if f.alive?
66
+ # no need to check if the fiber is already resumed or not,
67
+ # because monitor should have already handled this in the
68
+ # case of fibers
63
69
  }
64
70
 
65
71
  when /Coolio/
@@ -1,4 +1,10 @@
1
1
 
2
2
  class RestCore::Timeout::CoolioTimer < ::Coolio::TimerWatcher
3
3
  attr_accessor :error
4
+
5
+ alias_method :cancel, :detach
6
+
7
+ def canceled?
8
+ !attached?
9
+ end
4
10
  end
@@ -6,9 +6,19 @@ class RestCore::Timeout::EventMachineTimer < ::EventMachine::Timer
6
6
  super(timeout, &block) if block_given?
7
7
  self.timeout = timeout
8
8
  self.error = error
9
+ @canceled = false
9
10
  end
10
11
 
11
12
  def on_timeout &block
12
13
  send(:initialize, timeout, error, &block)
13
14
  end
15
+
16
+ def cancel
17
+ super
18
+ @canceled = true
19
+ end
20
+
21
+ def canceled?
22
+ @canceled
23
+ end
14
24
  end
@@ -1,4 +1,4 @@
1
1
 
2
2
  module RestCore
3
- VERSION = '1.0.1'
3
+ VERSION = '1.0.2'
4
4
  end
@@ -3,16 +3,21 @@ require 'rest-core'
3
3
 
4
4
  module RestCore::Wrapper
5
5
  include RestCore
6
+
7
+ module DefaultApp
8
+ def default_app
9
+ @default_app ||= RestCore::Dry
10
+ end
11
+ end
12
+
6
13
  def self.included mod
7
- mod.send(:attr_reader, :init, :middles, :wrapped)
14
+ mod.send(:extend, DefaultApp)
8
15
  class << mod
9
16
  attr_writer :default_app
10
- def default_app
11
- @default_app ||= RestCore::Dry
12
- end
13
17
  end
14
18
  end
15
19
 
20
+ attr_reader :init, :middles, :wrapped
16
21
  attr_writer :default_app
17
22
  def default_app
18
23
  @default_app ||= self.class.default_app
@@ -22,6 +27,7 @@ module RestCore::Wrapper
22
27
  @middles ||= []
23
28
  instance_eval(&block) if block_given?
24
29
  @wrapped ||= to_app
30
+ @init = nil
25
31
  end
26
32
 
27
33
  def use middle, *args, &block
@@ -43,9 +49,9 @@ module RestCore::Wrapper
43
49
  }.flatten
44
50
  end
45
51
 
46
- def to_app init=@init || default_app
52
+ def to_app app=init || default_app
47
53
  # === foldr m.new app middles
48
- middles.reverse.inject(init.new){ |app_, (middle, args, block)|
54
+ middles.reverse.inject(app.new){ |app_, (middle, args, block)|
49
55
  begin
50
56
  middle.new(app_, *partial_deep_copy(args), &block)
51
57
  rescue ArgumentError => e
@@ -2,13 +2,13 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "rest-core"
5
- s.version = "1.0.1"
5
+ s.version = "1.0.2"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = [
9
9
  "Cardinal Blue",
10
10
  "Lin Jen-Shin (godfat)"]
11
- s.date = "2012-05-14"
11
+ s.date = "2012-06-05"
12
12
  s.description = "Modular Ruby clients interface for REST APIs\n\nThere has been an explosion in the number of REST APIs available today.\nTo address the need for a way to access these APIs easily and elegantly,\nwe have developed [rest-core][], which consists of composable middleware\nthat allows you to build a REST client for any REST API. Or in the case of\ncommon APIs such as Facebook, Github, and Twitter, you can simply use the\ndedicated clients provided by [rest-more][].\n\n[rest-core]: https://github.com/cardinalblue/rest-core\n[rest-more]: https://github.com/cardinalblue/rest-more"
13
13
  s.email = ["dev (XD) cardinalblue.com"]
14
14
  s.files = [
@@ -78,10 +78,9 @@ Gem::Specification.new do |s|
78
78
  "pending/test_multi.rb",
79
79
  "pending/test_test_util.rb",
80
80
  "rest-core.gemspec",
81
- "task/.gitignore",
82
- "task/gemgem.rb",
83
81
  "test/test_auth_basic.rb",
84
82
  "test/test_builder.rb",
83
+ "test/test_cache.rb",
85
84
  "test/test_client.rb",
86
85
  "test/test_client_oauth1.rb",
87
86
  "test/test_default_query.rb",
@@ -101,6 +100,7 @@ Gem::Specification.new do |s|
101
100
  s.test_files = [
102
101
  "test/test_auth_basic.rb",
103
102
  "test/test_builder.rb",
103
+ "test/test_cache.rb",
104
104
  "test/test_client.rb",
105
105
  "test/test_client_oauth1.rb",
106
106
  "test/test_default_query.rb",
@@ -0,0 +1,69 @@
1
+
2
+ require 'rest-core/test'
3
+
4
+ describe RC::Cache do
5
+ after do
6
+ WebMock.reset!
7
+ RR.verify
8
+ end
9
+
10
+ should 'basic' do
11
+ c = RC::Builder.client do
12
+ use RC::Cache, {}, 3600
13
+ run Class.new{
14
+ attr_accessor :tick
15
+ def initialize
16
+ self.tick = 0
17
+ end
18
+ def call env
19
+ self.tick +=1
20
+ env.merge(RC::RESPONSE_BODY => 'response')
21
+ end
22
+ }
23
+ end.new
24
+ c.get('/')
25
+ c.cache.should.eq({Digest::MD5.hexdigest('/') => 'response'})
26
+ c.app.app.tick.should.eq 1
27
+ c.get('/')
28
+ c.app.app.tick.should.eq 1
29
+ c.cache.clear
30
+ c.get('/')
31
+ c.app.app.tick.should.eq 2
32
+ end
33
+
34
+ should 'cancel timeout for fiber' do
35
+ any_instance_of(RC::Timeout::EventMachineTimer) do |timer|
36
+ proxy.mock(timer).cancel.times(2)
37
+ end
38
+ path = 'http://example.com/'
39
+ stub_request(:get, path).to_return(:body => 'response')
40
+ c = RC::Builder.client do
41
+ use RC::Timeout, 10
42
+ use RC::Cache, {}, 3600
43
+ run RC::EmHttpRequestFiber
44
+ end.new
45
+ EM.run{ Fiber.new{
46
+ c.request_full(RC::REQUEST_PATH => path)
47
+ c.request_full(RC::REQUEST_PATH => path)
48
+ EM.stop }.resume }
49
+ c.cache.size.should.eq 1
50
+ end if defined?(Fiber)
51
+
52
+ should 'cancel timeout for async' do
53
+ path = 'http://example.com/'
54
+ any_instance_of(RC::Timeout::EventMachineTimer) do |timer|
55
+ mock(timer).cancel.times(2)
56
+ end
57
+ stub_request(:get, path).to_return(:body => 'response')
58
+ c = RC::Builder.client do
59
+ use RC::Timeout, 10
60
+ use RC::Cache, {}, 3600
61
+ run RC::EmHttpRequestAsync
62
+ end.new
63
+ EM.run{
64
+ c.request_full(RC::REQUEST_PATH => path){
65
+ c.request_full(RC::REQUEST_PATH => path){
66
+ EM.stop }}}
67
+ c.cache.size.should.eq 1
68
+ end
69
+ end
@@ -6,6 +6,10 @@ describe RC::Timeout do
6
6
  @app = RC::Timeout.new(RC::Dry.new, 0)
7
7
  end
8
8
 
9
+ after do
10
+ WebMock.reset!
11
+ end
12
+
9
13
  should 'bypass timeout if timeout is 0' do
10
14
  mock(@app).monitor.times(0)
11
15
  @app.call({}).should.eq({})
@@ -16,4 +20,15 @@ describe RC::Timeout do
16
20
  mock.proxy(@app).monitor(env).times(1)
17
21
  @app.call(env).should.eq(env)
18
22
  end
23
+
24
+ should 'return correct result under fibers' do
25
+ path = 'http://example.com/'
26
+ stub_request(:get, path).to_return(:body => 'response')
27
+
28
+ c = RC::Builder.client do
29
+ use RC::Timeout, 10
30
+ run RC::EmHttpRequestFiber
31
+ end.new
32
+ EM.run{Fiber.new{c.get(path).should.eq('response');EM.stop}.resume}
33
+ end if defined?(Fiber)
19
34
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-05-14 00:00:00.000000000 Z
13
+ date: 2012-06-05 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rest-client
@@ -119,10 +119,9 @@ files:
119
119
  - pending/test_multi.rb
120
120
  - pending/test_test_util.rb
121
121
  - rest-core.gemspec
122
- - task/.gitignore
123
- - task/gemgem.rb
124
122
  - test/test_auth_basic.rb
125
123
  - test/test_builder.rb
124
+ - test/test_cache.rb
126
125
  - test/test_client.rb
127
126
  - test/test_client_oauth1.rb
128
127
  - test/test_default_query.rb
@@ -162,6 +161,7 @@ summary: Modular Ruby clients interface for REST APIs
162
161
  test_files:
163
162
  - test/test_auth_basic.rb
164
163
  - test/test_builder.rb
164
+ - test/test_cache.rb
165
165
  - test/test_client.rb
166
166
  - test/test_client_oauth1.rb
167
167
  - test/test_default_query.rb
@@ -1 +0,0 @@
1
- *.rbc
@@ -1,257 +0,0 @@
1
-
2
- require 'pathname'
3
-
4
- module Gemgem
5
- class << self
6
- attr_accessor :dir, :spec
7
- end
8
-
9
- module_function
10
- def create
11
- yield(spec = Gem::Specification.new{ |s|
12
- s.authors = ['Lin Jen-Shin (godfat)']
13
- s.email = ['godfat (XD) godfat.org']
14
-
15
- s.description = description.join
16
- s.summary = description.first
17
-
18
- s.rubygems_version = Gem::VERSION
19
- s.date = Time.now.strftime('%Y-%m-%d')
20
- s.files = gem_files
21
- s.test_files = gem_files.grep(%r{^test/(.+?/)*test_.+?\.rb$})
22
- s.executables = Dir['bin/*'].map{ |f| File.basename(f) }
23
- s.require_paths = %w[lib]
24
- })
25
- spec.homepage ||= "https://github.com/godfat/#{spec.name}"
26
- spec
27
- end
28
-
29
- def readme
30
- path = %w[README.md README].find{ |name|
31
- File.exist?("#{Gemgem.dir}/#{name}")
32
- }
33
- @readme ||=
34
- if path
35
- ps = File.read(path).scan(/#+[^\n]+\n\n.+?(?=\n\n#+[^\n]+\n)/m)
36
- ps.inject({'HEADER' => ps.first}){ |r, s, i|
37
- r[s[/\w+/]] = s
38
- r
39
- }
40
- else
41
- {}
42
- end
43
- end
44
-
45
- def description
46
- @description ||= (readme['DESCRIPTION']||'').sub(/.+\n\n/, '').lines.to_a
47
- end
48
-
49
- def changes
50
- path = %w[CHANGES.md CHANGES].find{ |name|
51
- File.exist?("#{Gemgem.dir}/#{name}")
52
- }
53
- @changes ||=
54
- if path
55
- date = '\d+{4}\-\d+{2}\-\d{2}'
56
- File.read(path).match(
57
- /([^\n]+#{date}\n\n(.+?))(?=\n\n[^\n]+#{date}\n)/m)[1]
58
- else
59
- ''
60
- end
61
- end
62
-
63
- def ann_md
64
- "##{readme['HEADER'].sub(/([\w\-]+)/, "[\\1](#{spec.homepage})")}\n\n" \
65
- "##{readme['DESCRIPTION'][/[^\n]+\n\n[^\n]+/]}\n\n" \
66
- "### CHANGES:\n\n" \
67
- "###{changes}\n\n" \
68
- "##{readme['INSTALLATION']}\n\n" +
69
- if readme['SYNOPSIS'] then "##{readme['SYNOPSIS']}" else '' end
70
- end
71
-
72
- def ann_html
73
- gem 'nokogiri'
74
- gem 'kramdown'
75
-
76
- IO.popen('kramdown', 'r+') do |md|
77
- md.puts Gemgem.ann_md
78
- md.close_write
79
- require 'nokogiri'
80
- html = Nokogiri::XML.parse("<gemgem>#{md.read}</gemgem>")
81
- html.css('*').each{ |n| n.delete('id') }
82
- html.root.children.to_html
83
- end
84
- end
85
-
86
- def ann_email
87
- "#{readme['HEADER'].sub(/([\w\-]+)/, "\\1 <#{spec.homepage}>")}\n\n" \
88
- "#{readme['DESCRIPTION']}\n\n" \
89
- "#{readme['INSTALLATION']}\n\n" +
90
- if readme['SYNOPSIS'] then "##{readme['SYNOPSIS']}\n\n" else '' end +
91
- "## CHANGES:\n\n" \
92
- "##{changes}\n\n"
93
- end
94
-
95
- def gem_tag
96
- "#{spec.name}-#{spec.version}"
97
- end
98
-
99
- def write
100
- File.open("#{dir}/#{spec.name}.gemspec", 'w'){ |f|
101
- f << split_lines(spec.to_ruby) }
102
- end
103
-
104
- def split_lines ruby
105
- ruby.gsub(/(.+?)\[(.+?)\]/){ |s|
106
- if $2.index(',')
107
- "#{$1}[\n #{$2.split(',').map(&:strip).join(",\n ")}]"
108
- else
109
- s
110
- end
111
- }
112
- end
113
-
114
- def all_files
115
- @all_files ||= find_files(Pathname.new(dir)).map{ |file|
116
- if file.to_s =~ %r{\.git/}
117
- nil
118
- else
119
- file.to_s
120
- end
121
- }.compact.sort
122
- end
123
-
124
- def gem_files
125
- @gem_files ||= all_files - ignored_files
126
- end
127
-
128
- def ignored_files
129
- @ignored_file ||= all_files.select{ |path| ignore_patterns.find{ |ignore|
130
- path =~ ignore && !git_files.include?(path)}}
131
- end
132
-
133
- def git_files
134
- @git_files ||= if File.exist?("#{dir}/.git")
135
- `git ls-files`.split("\n")
136
- else
137
- []
138
- end
139
- end
140
-
141
- # protected
142
- def find_files path
143
- path.children.select(&:file?).map{|file| file.to_s[(dir.size+1)..-1]} +
144
- path.children.select(&:directory?).map{|dir| find_files(dir)}.flatten
145
- end
146
-
147
- def ignore_patterns
148
- @ignore_files ||= expand_patterns(
149
- File.read("#{dir}/.gitignore").split("\n").reject{ |pattern|
150
- pattern.strip == ''
151
- }).map{ |pattern| %r{^([^/]+/)*?#{Regexp.escape(pattern)}(/[^/]+)*?$} }
152
- end
153
-
154
- def expand_patterns pathes
155
- pathes.map{ |path|
156
- if path !~ /\*/
157
- path
158
- else
159
- expand_patterns(
160
- Dir[path] +
161
- Pathname.new(File.dirname(path)).children.select(&:directory?).
162
- map{ |prefix| "#{prefix}/#{File.basename(path)}" })
163
- end
164
- }.flatten
165
- end
166
- end
167
-
168
- namespace :gem do
169
-
170
- desc 'Install gem'
171
- task :install => [:build] do
172
- sh("#{Gem.ruby} -S gem install pkg/#{Gemgem.gem_tag}")
173
- end
174
-
175
- desc 'Build gem'
176
- task :build => [:spec] do
177
- sh("#{Gem.ruby} -S gem build #{Gemgem.spec.name}.gemspec")
178
- sh("mkdir -p pkg")
179
- sh("mv #{Gemgem.gem_tag}.gem pkg/")
180
- end
181
-
182
- desc 'Release gem'
183
- task :release => [:spec, :check, :build] do
184
- sh("git tag #{Gemgem.gem_tag}")
185
- sh("git push")
186
- sh("git push --tags")
187
- sh("#{Gem.ruby} -S gem push pkg/#{Gemgem.gem_tag}.gem")
188
- end
189
-
190
- task :check do
191
- ver = Gemgem.spec.version.to_s
192
-
193
- if ENV['VERSION'].nil?
194
- puts("\x1b[35mExpected " \
195
- "\x1b[33mVERSION\x1b[35m=\x1b[33m#{ver}\x1b[m")
196
- exit(1)
197
-
198
- elsif ENV['VERSION'] != ver
199
- puts("\x1b[35mExpected \x1b[33mVERSION\x1b[35m=\x1b[33m#{ver} " \
200
- "\x1b[35mbut got\n " \
201
- "\x1b[33mVERSION\x1b[35m=\x1b[33m#{ENV['VERSION']}\x1b[m")
202
- exit(2)
203
- end
204
- end
205
-
206
- end # of gem namespace
207
-
208
- desc 'Run tests in memory'
209
- task :test do
210
- require 'bacon'
211
- Bacon.extend(Bacon::TestUnitOutput)
212
- Bacon.summary_on_exit
213
- $LOAD_PATH.unshift('lib')
214
- Dir['./test/**/test_*.rb'].each{ |file| require file[0..-4] }
215
- end
216
-
217
- desc 'Run tests with shell'
218
- task 'test:shell', :RUBY_OPTS do |t, args|
219
- files = Dir['test/**/test_*.rb'].join(' ')
220
-
221
- cmd = [Gem.ruby, args[:RUBY_OPTS],
222
- '-I', 'lib', '-S', 'bacon', '--quiet', files]
223
-
224
- sh(cmd.compact.join(' '))
225
- end
226
-
227
- desc 'Generate ann markdown'
228
- task 'ann:md' => ['gem:spec'] do
229
- puts Gemgem.ann_md
230
- end
231
-
232
- desc 'Generate ann html'
233
- task 'ann:html' => ['gem:spec'] do
234
- puts Gemgem.ann_html
235
- end
236
-
237
- desc 'Generate ann email'
238
- task 'ann:email' => ['gem:spec'] do
239
- puts Gemgem.ann_email
240
- end
241
-
242
- desc 'Generate rdoc'
243
- task :doc => ['gem:spec'] do
244
- sh("yardoc -o rdoc --main README.md" \
245
- " --files #{Gemgem.spec.extra_rdoc_files.join(',')}")
246
- end
247
-
248
- desc 'Remove ignored files'
249
- task :clean => ['gem:spec'] do
250
- trash = "~/.Trash/#{Gemgem.spec.name}/"
251
- sh "mkdir -p #{trash}" unless File.exist?(File.expand_path(trash))
252
- Gemgem.ignored_files.each{ |file| sh "mv #{file} #{trash}" }
253
- end
254
-
255
- task :default do
256
- puts `#{Gem.ruby} -S #{$PROGRAM_NAME} -T`
257
- end