gh 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -13,13 +13,11 @@ This will by default use all the middleware that ships with GH, in the following
13
13
 
14
14
  * `GH::Remote` - sends HTTP requests to GitHub and parses the response
15
15
  * `GH::Normalizer` - renames fields consistenly, adds hypermedia links if possible
16
+ * `GH::Cache` - caches the responses (will use Rails cache if in Rails, in-memory cache otherwise)
16
17
  * `GH::LazyLoader` - will load missing fields when accessed (handy for dealing with incomplete data without sending to many requests)
17
- * `GH::LinkFollower` - will add content of hypermedia links as fields (lazyly), allows you to traverse relations
18
18
  * `GH::MergeCommit` - adds infos about merge commits to pull request payloads
19
-
20
- This middleware ships with the `gh` library but is not added by default:
21
-
22
- * `GH::Cache` - caches the responses (will use Rails cache if in Rails, in-memory cache otherwise)
19
+ * `GH::LinkFollower` - will add content of hypermedia links as fields (lazyly), allows you to traverse relations
20
+ * `GH::Instrumentation` - let's you instrument `gh`
23
21
 
24
22
  ## Main Entry Points
25
23
 
data/gh.gemspec CHANGED
@@ -19,7 +19,8 @@ Gem::Specification.new do |s|
19
19
  s.add_development_dependency 'rspec'
20
20
  s.add_development_dependency 'webmock'
21
21
 
22
- s.add_runtime_dependency 'faraday', '~> 0.7'
22
+ s.add_runtime_dependency 'faraday', '~> 0.8'
23
23
  s.add_runtime_dependency 'backports', '~> 2.3'
24
24
  s.add_runtime_dependency 'multi_json', '~> 1.0'
25
+ s.add_runtime_dependency 'addressable'
25
26
  end
data/lib/gh.rb CHANGED
@@ -5,10 +5,13 @@ require 'forwardable'
5
5
  module GH
6
6
  autoload :Cache, 'gh/cache'
7
7
  autoload :Case, 'gh/case'
8
+ autoload :Error, 'gh/error'
9
+ autoload :Instrumentation, 'gh/instrumentation'
8
10
  autoload :LazyLoader, 'gh/lazy_loader'
9
11
  autoload :LinkFollower, 'gh/link_follower'
10
12
  autoload :MergeCommit, 'gh/merge_commit'
11
13
  autoload :Normalizer, 'gh/normalizer'
14
+ autoload :Pagination, 'gh/pagination'
12
15
  autoload :Remote, 'gh/remote'
13
16
  autoload :Response, 'gh/response'
14
17
  autoload :ResponseWrapper, 'gh/response_wrapper'
@@ -32,13 +35,15 @@ module GH
32
35
  end
33
36
 
34
37
  extend SingleForwardable
35
- def_delegators :current, :api_host, :[], :reset, :load, :post
38
+ def_delegators :current, :api_host, :[], :reset, :load, :post, :delete, :patch, :put
36
39
 
37
40
  DefaultStack = Stack.new do
41
+ use Instrumentation
42
+ use Pagination
38
43
  use LinkFollower
39
44
  use MergeCommit
40
45
  use LazyLoader
41
- #use Cache
46
+ use Cache
42
47
  use Normalizer
43
48
  use Remote
44
49
  end
@@ -0,0 +1,17 @@
1
+ require 'gh'
2
+
3
+ module GH
4
+ class Error < Exception
5
+ attr_reader :error, :payload
6
+
7
+ def initialize(error = nil, payload = nil)
8
+ error = error.error while error.respond_to? :error
9
+ @error, @payload = error, payload
10
+ set_backtrace error.backtrace if error
11
+ end
12
+
13
+ def message
14
+ "GH request failed (#{error.message}) with payload: #{payload.inspect}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ require 'gh'
2
+
3
+ module GH
4
+ # Public: This class caches responses.
5
+ class Instrumentation < Wrapper
6
+ # Public: Get/set instrumenter to use. Compatible with ActiveSupport::Notification and Travis::EventLogger.
7
+ attr_accessor :instrumenter
8
+
9
+ def setup(backend, options)
10
+ self.instrumenter ||= Travis::EventLogger.method(:notify) if defined? Travis::EventLogger
11
+ self.instrumenter ||= ActiveSupport::Notifications.method(:instrument) if defined? ActiveSupport::Notifications
12
+ super
13
+ end
14
+
15
+ def http(verb, url, *)
16
+ instrument(:http, :verb => verb, :url => url) { super }
17
+ end
18
+
19
+ def load(data)
20
+ instrument(:load, :data => data) { super }
21
+ end
22
+
23
+ def [](key)
24
+ instrument(:access, :key => key) { super }
25
+ end
26
+
27
+ private
28
+
29
+ def instrument(type, payload = {})
30
+ return yield unless instrumenter
31
+ result = nil
32
+ instrumenter.call("#{type}.gh", payload.merge(:gh => frontend)) { result = yield }
33
+ return result
34
+ end
35
+ end
36
+ end
@@ -11,12 +11,16 @@ module GH
11
11
  link = hash['_links'].try(:[], 'self') unless loaded
12
12
  setup_lazy_loading(hash, link['href']) if link
13
13
  hash
14
+ rescue Exception => error
15
+ raise Error.new(error, hash)
14
16
  end
15
17
 
16
18
  private
17
19
 
18
20
  def lazy_load(hash, key, link)
19
21
  result = modify_hash(backend[link].data, true)
22
+ rescue Exception => error
23
+ raise Error.new(error, hash)
20
24
  end
21
25
  end
22
26
  end
@@ -7,6 +7,8 @@ module GH
7
7
  hash = super
8
8
  setup_lazy_loading(hash) if hash['_links']
9
9
  hash
10
+ rescue Exception => error
11
+ raise Error.new(error, hash)
10
12
  end
11
13
 
12
14
  private
@@ -14,6 +16,8 @@ module GH
14
16
  def lazy_load(hash, key)
15
17
  link = hash['_links'][key]
16
18
  { key => self[link['href']] } if link
19
+ rescue Exception => error
20
+ raise Error.new(error, hash)
17
21
  end
18
22
  end
19
23
  end
@@ -1,4 +1,5 @@
1
1
  require 'gh'
2
+ require 'timeout'
2
3
 
3
4
  module GH
4
5
  # Public: ...
@@ -13,24 +14,34 @@ module GH
13
14
 
14
15
  def modify_hash(hash)
15
16
  setup_lazy_loading(super)
17
+ rescue Exception => error
18
+ raise Error.new(error, hash)
16
19
  end
17
20
 
18
21
  private
19
22
 
20
23
  def lazy_load(hash, key)
21
- return unless key =~ /^(merge|head)_commit$/ and hash.include? 'mergeable'
22
-
23
- # FIXME: Rick said "this will become part of the API"
24
- # until then, please look the other way
25
- while hash['mergable'].nil?
26
- url = hash['_links']['html']['href'] + '/mergeable'
27
- case http(:get, url).body
28
- when "true" then hash['mergable'] = true
29
- when "false" then hash['mergable'] = false
30
- end
31
- end
24
+ return unless key =~ /^(merge|head|base)_commit$/ and hash.include? 'mergeable'
25
+ return unless force_merge_commit(hash)
26
+ fields = pull_request_refs(hash)
27
+ fields['base_commit'] ||= commit_for hash, hash['base']
28
+ fields['head_commit'] ||= commit_for hash, hash['head']
29
+ fields
30
+ rescue Exception => error
31
+ raise Error.new(error, hash)
32
+ end
32
33
 
33
- link = hash['_links']['self']['href'].gsub(%r{/pulls/(\d+)$}, '/git/refs/pull/\1')
34
+ def commit_for(from, hash)
35
+ { 'sha' => hash['sha'], 'ref' => hash['ref'],
36
+ '_links' => { 'self' => { 'href' => git_url_for(from, hash['sha']) } } }
37
+ end
38
+
39
+ def git_url_for(hash, commitish)
40
+ hash['_links']['self']['href'].gsub(%r{/pulls/(\d+)$}, "/git/#{commitish}")
41
+ end
42
+
43
+ def pull_request_refs(hash)
44
+ link = git_url_for(hash, 'refs/pull/\1')
34
45
  commits = self[link].map do |data|
35
46
  ref = data['ref']
36
47
  name = ref.split('/').last + "_commit"
@@ -39,5 +50,20 @@ module GH
39
50
  end
40
51
  Hash[commits]
41
52
  end
53
+
54
+ def force_merge_commit(hash)
55
+ Timeout.timeout(10) do # MAGIC NUMBERS FTW
56
+ # FIXME: Rick said "this will become part of the API"
57
+ # until then, please look the other way
58
+ while hash['mergeable'].nil?
59
+ url = hash['_links']['html']['href'] + '/mergeable'
60
+ case frontend.http(:get, url).body
61
+ when "true" then hash['mergeable'] = true
62
+ when "false" then hash['mergeable'] = false
63
+ end
64
+ end
65
+ end
66
+ hash['mergeable']
67
+ end
42
68
  end
43
69
  end
@@ -86,6 +86,8 @@ module GH
86
86
  set_link(hash, type, value)
87
87
  when /^(.+)_url$/
88
88
  set_link(hash, $1, value)
89
+ when "config"
90
+ hash[key] = value
89
91
  end
90
92
  end
91
93
 
@@ -1,4 +1,35 @@
1
1
  module GH
2
2
  class Pagination < Wrapper
3
+ class Paginated
4
+ include Enumerable
5
+
6
+ def initialize(page, url, gh)
7
+ @page, @next_url, @gh = page, url, gh
8
+ end
9
+
10
+ def each(&block)
11
+ return enum_for(:each) unless block
12
+ @page.each(&block)
13
+ next_page.each(&block)
14
+ end
15
+
16
+ def inspect
17
+ "[#{first.inspect}, ...]"
18
+ end
19
+
20
+ private
21
+
22
+ def next_page
23
+ @next_page ||= @gh[@next_url]
24
+ end
25
+ end
26
+
27
+ wraps GH::Normalizer
28
+ double_dispatch
29
+
30
+ def modify_response(response)
31
+ return response unless response.headers['link'] =~ /<([^>]+)>;\s*rel=\"next\"/
32
+ Paginated.new(response, $1, self)
33
+ end
3
34
  end
4
35
  end
@@ -1,5 +1,5 @@
1
1
  require 'gh'
2
- require 'gh/faraday'
2
+ require 'faraday'
3
3
 
4
4
  module GH
5
5
  # Public: This class deals with HTTP requests to Github. It is the base Wrapper you always want to use.
@@ -43,7 +43,7 @@ module GH
43
43
  faraday_options.merge! options[:faraday_options] if options[:faraday_options]
44
44
 
45
45
  @connection = Faraday.new(faraday_options) do |builder|
46
- builder.request(:token_auth, token) if token
46
+ builder.request(:authorization, :token, token) if token
47
47
  builder.request(:basic_auth, username, password) if username and password
48
48
  builder.request(:retry)
49
49
  builder.response(:raise_error)
@@ -66,7 +66,7 @@ module GH
66
66
  # Raises Faraday::Error::ClientError if the resource returns a status between 400 and 599.
67
67
  # Returns the Response.
68
68
  def [](key)
69
- response = http(:get, path_for(key), headers)
69
+ response = frontend.http(:get, path_for(key), headers)
70
70
  modify(response.body, response.headers)
71
71
  end
72
72
 
@@ -75,14 +75,34 @@ module GH
75
75
  connection.run_request(verb, url, nil, headers, &block)
76
76
  end
77
77
 
78
- # Public: ...
79
- def post(key, body)
80
- response = http(:post, path_for(key), headers) do |req|
81
- req.body = Response.new({}, body).to_s
78
+ # Internal: ...
79
+ def request(verb, key, body = nil)
80
+ response = frontend.http(verb, path_for(key), headers) do |req|
81
+ req.body = Response.new({}, body).to_s if body
82
82
  end
83
83
  modify(response.body, response.headers)
84
84
  end
85
85
 
86
+ # Public: ...
87
+ def post(key, body)
88
+ request(:post, key, body)
89
+ end
90
+
91
+ # Public: ...
92
+ def delete(key)
93
+ request(:delete, key)
94
+ end
95
+
96
+ # Public: ...
97
+ def patch(key, body)
98
+ request(:patch, key, body)
99
+ end
100
+
101
+ # Public: ...
102
+ def put(key, body)
103
+ request(:put, key, body)
104
+ end
105
+
86
106
  # Public: ...
87
107
  def reset
88
108
  end
@@ -31,6 +31,7 @@ module GH
31
31
  when respond_to(:to_str) then @body = body.to_str
32
32
  when respond_to(:to_hash) then @data = body.to_hash
33
33
  when respond_to(:to_ary) then @data = body.to_ary
34
+ when nil then @data = {}
34
35
  else raise ArgumentError, "cannot parse #{body.inspect}"
35
36
  end
36
37
 
@@ -74,6 +75,11 @@ module GH
74
75
  self
75
76
  end
76
77
 
78
+ # Public: ...
79
+ def ==(other)
80
+ super or @data == other
81
+ end
82
+
77
83
  protected
78
84
 
79
85
  def dup_ivars
@@ -1,4 +1,4 @@
1
1
  module GH
2
2
  # Public: Library version.
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
  end
@@ -36,6 +36,15 @@ module GH
36
36
  # Public: ...
37
37
  def_delegator :backend, :post
38
38
 
39
+ # Public: ...
40
+ def_delegator :backend, :delete
41
+
42
+ # Public: ...
43
+ def_delegator :backend, :patch
44
+
45
+ # Public: ...
46
+ def_delegator :backendt, :put
47
+
39
48
  # Public: Retrieves resources from Github.
40
49
  def self.[](key)
41
50
  new[key]
@@ -124,6 +133,8 @@ module GH
124
133
  when respond_to(:to_int) then modify_integer(data)
125
134
  else modify_unkown data
126
135
  end
136
+ rescue Exception => error
137
+ raise Error.new(error, data)
127
138
  end
128
139
 
129
140
  def modify_response(response)
@@ -133,6 +144,8 @@ module GH
133
144
 
134
145
  def modify(data, *)
135
146
  data
147
+ rescue Exception => error
148
+ raise Error.new(error, data)
136
149
  end
137
150
 
138
151
  def modify_array(array)
@@ -182,6 +195,7 @@ module GH
182
195
  next if loaded
183
196
  fields = lazy_load(hash, key, *args)
184
197
  if fields
198
+ modify_hash fields
185
199
  hash.merge! fields
186
200
  loaded = true
187
201
  fields[key]
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe GH::Error do
4
+ class SomeWrapper < GH::Wrapper
5
+ double_dispatch
6
+ def modify_hash(*)
7
+ raise "foo"
8
+ end
9
+ end
10
+
11
+ let(:exception) do
12
+ begin
13
+ SomeWrapper.new.load('foo' => 'bar')
14
+ nil
15
+ rescue Exception => error
16
+ error
17
+ end
18
+ end
19
+
20
+ it "wraps connection" do
21
+ exception.should be_an(GH::Error)
22
+ end
23
+
24
+ it "exposes the original exception" do
25
+ exception.error.should be_a(RuntimeError)
26
+ end
27
+
28
+ it 'keeps the payload around' do
29
+ exception.payload.should be == {'foo' => 'bar'}
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe GH::Instrumentation do
4
+ before do
5
+ @events = []
6
+ subject.instrumenter = proc { |*a, &b| @events << a and b[] }
7
+ stub_request(:get, "https://api.github.com/").to_return :body => "{}"
8
+ end
9
+
10
+ it 'instruments http' do
11
+ subject.http :get, '/'
12
+ @events.size.should be == 1
13
+ @events.first.should be == ['http.gh', {:verb => :get, :url => '/', :gh => subject}]
14
+ end
15
+
16
+ it 'instruments []' do
17
+ subject['/']
18
+ @events.size.should be == 2
19
+ @events.should be == [
20
+ ['access.gh', {:key => '/', :gh => subject}],
21
+ ['http.gh', {:verb => :get, :url => '/', :gh => subject}]
22
+ ]
23
+ end
24
+
25
+ it 'instruments load' do
26
+ subject.load("[]")
27
+ @events.size.should be == 1
28
+ @events.first.should be == ['load.gh', {:data => "[]", :gh => subject}]
29
+ end
30
+ end