gh 0.3.0 → 0.4.0

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