gh 0.0.1
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/.gitignore +4 -0
- data/.travis.yml +12 -0
- data/Gemfile +6 -0
- data/README.md +25 -0
- data/Rakefile +6 -0
- data/gh.gemspec +25 -0
- data/lib/gh.rb +32 -0
- data/lib/gh/cache.rb +47 -0
- data/lib/gh/case.rb +8 -0
- data/lib/gh/faraday.rb +99 -0
- data/lib/gh/normalizer.rb +109 -0
- data/lib/gh/remote.rb +71 -0
- data/lib/gh/response.rb +89 -0
- data/lib/gh/stack.rb +52 -0
- data/lib/gh/version.rb +4 -0
- data/lib/gh/wrapper.rb +102 -0
- data/spec/cache_spec.rb +17 -0
- data/spec/normalizer_spec.rb +260 -0
- data/spec/payloads/users/rkh.yml +22 -0
- data/spec/payloads/users/svenfuchs.yml +21 -0
- data/spec/remote_spec.rb +23 -0
- data/spec/response_spec.rb +5 -0
- data/spec/spec_helper.rb +62 -0
- data/spec/stack_spec.rb +5 -0
- data/spec/wrapper_spec.rb +5 -0
- metadata +134 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
**This is work in progress and not yet usable!**
|
2
|
+
|
3
|
+
Goal of this library is to ease usage of the Github API as part of large, tightly integrated, distributed projects, such as [Travis CI](http://travis-ci.org). It was born out due to issues we ran into with all existing Github libraries and the Github API itself.
|
4
|
+
|
5
|
+
With that in mind, this library follows the following goals:
|
6
|
+
|
7
|
+
* Implement features in separate layers, make layers as independend of each other as possible
|
8
|
+
* Higher level layers should not worry about when to send requests
|
9
|
+
* It should only send requests to Github when necessary
|
10
|
+
* It should be able to fetch data from Github asynchronously (i.e. HTTP requests to Travis should not be bound to HTTP requests to Github, if possible)
|
11
|
+
* It should be able to deal with events and hooks well (i.e. update cached entities with hook data)
|
12
|
+
* It should not have intransparent magic (i.e. implicit, undocumented requirements on fields we get from Github)
|
13
|
+
* It should shield against possible changes to the Github API or at least complain about those changes if it can't deal with it.
|
14
|
+
|
15
|
+
Most of this is not yet implemented!
|
16
|
+
|
17
|
+
The lower level APIs support a Rack-like stacking API:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
api = GH::Stack.build do
|
21
|
+
use GH::Cache, cache: Rails.cache
|
22
|
+
use GH::Normalizer
|
23
|
+
use GH::Remote, username: "admin", password: "admin"
|
24
|
+
end
|
25
|
+
```
|
data/Rakefile
ADDED
data/gh.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "gh/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "gh"
|
7
|
+
s.version = GH::VERSION
|
8
|
+
s.authors = ["Konstantin Haase"]
|
9
|
+
s.email = ["konstantin.mailinglists@googlemail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{layered github client}
|
12
|
+
s.description = %q{multi-layer client for the github api v3}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency 'rspec'
|
20
|
+
s.add_development_dependency 'webmock'
|
21
|
+
|
22
|
+
s.add_runtime_dependency 'faraday', '~> 0.7'
|
23
|
+
s.add_runtime_dependency 'backports', '~> 2.3'
|
24
|
+
s.add_runtime_dependency 'multi_json', '~> 1.0'
|
25
|
+
end
|
data/lib/gh.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'gh/version'
|
2
|
+
require 'backports'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module GH
|
6
|
+
autoload :Cache, 'gh/cache'
|
7
|
+
autoload :Case, 'gh/case'
|
8
|
+
autoload :Normalizer, 'gh/normalizer'
|
9
|
+
autoload :Remote, 'gh/remote'
|
10
|
+
autoload :Response, 'gh/response'
|
11
|
+
autoload :Stack, 'gh/stack'
|
12
|
+
autoload :Wrapper, 'gh/wrapper'
|
13
|
+
|
14
|
+
def self.[](key)
|
15
|
+
backend = Thread.current[:GH] ||= DefaultStack.build
|
16
|
+
backend[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.with(backend)
|
20
|
+
backend = DefaultStack.build(backend) if Hash === backend
|
21
|
+
was, Thread.current[:GH] = Thread.current[:GH], backend
|
22
|
+
yield
|
23
|
+
ensure
|
24
|
+
Thread.current[:GH] = was
|
25
|
+
end
|
26
|
+
|
27
|
+
DefaultStack = Stack.new do
|
28
|
+
use Cache
|
29
|
+
use Normalizer
|
30
|
+
use Remote
|
31
|
+
end
|
32
|
+
end
|
data/lib/gh/cache.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: This class deals with HTTP requests to Github. It is the base Wrapper you always want to use.
|
6
|
+
# Note that it is usually used implicitely by other wrapper classes if not specified.
|
7
|
+
class Cache < Wrapper
|
8
|
+
# Public: Get/set cache to use. Compatible with Rails/ActiveSupport cache.
|
9
|
+
attr_accessor :cache
|
10
|
+
|
11
|
+
# Internal: Simple in-memory cache basically implementing a copying GC.
|
12
|
+
class SimpleCache
|
13
|
+
# Internal: Initializes a new SimpleCache.
|
14
|
+
#
|
15
|
+
# size - Number of objects to hold in cache.
|
16
|
+
def initialize(size = 2048)
|
17
|
+
@old, @new, @size, @mutex = {}, {}, size/2, Mutex.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Internal: Tries to fetch a value from the cache and if it doesn't exist, generates it from the
|
21
|
+
# block given.
|
22
|
+
def fetch(key)
|
23
|
+
@mutex.lock { @old, @new = @new, {} if @new.size > @size } if @new.size > @size
|
24
|
+
@new[key] ||= @old[key] || yield
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Internal: Initializes a new Cache instance.
|
29
|
+
def setup(*)
|
30
|
+
self.cache ||= Rails.cache if defined? Rails.cache and defined? RAILS_CACHE
|
31
|
+
self.cache ||= ActiveSupport::Cache.lookup_store if defined? ActiveSupport::Cache.lookup_store
|
32
|
+
self.cache ||= SimpleCache.new
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
# Public: Retrieves resources from Github and caches response for future access.
|
37
|
+
#
|
38
|
+
# Examples
|
39
|
+
#
|
40
|
+
# Github::Cache.new['users/rkh'] # => { ... }
|
41
|
+
#
|
42
|
+
# Returns the Response.
|
43
|
+
def [](key)
|
44
|
+
cache.fetch(path_for(key)) { super }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/gh/case.rb
ADDED
data/lib/gh/faraday.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Copyright (c) 2009 rick olson, zack hobson
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
# this software and associated documentation files (the "Software"), to deal in
|
5
|
+
# the Software without restriction, including without limitation the rights to
|
6
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
# of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
# so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
11
|
+
# copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
# SOFTWARE.
|
20
|
+
|
21
|
+
require 'faraday'
|
22
|
+
|
23
|
+
if Faraday::VERSION < '0.8.0'
|
24
|
+
$stderr.puts "please update faraday"
|
25
|
+
|
26
|
+
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request/basic_authentication.rb
|
27
|
+
require 'base64'
|
28
|
+
|
29
|
+
module Faraday
|
30
|
+
class Request::BasicAuthentication < Faraday::Middleware
|
31
|
+
def initialize(app, login, pass)
|
32
|
+
super(app)
|
33
|
+
@header_value = "Basic #{Base64.encode64([login, pass].join(':')).gsub("\n", '')}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(env)
|
37
|
+
unless env[:request_headers]['Authorization']
|
38
|
+
env[:request_headers]['Authorization'] = @header_value
|
39
|
+
end
|
40
|
+
@app.call(env)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request/retry.rb
|
46
|
+
module Faraday
|
47
|
+
class Request::Retry < Faraday::Middleware
|
48
|
+
def initialize(app, retries = 2)
|
49
|
+
@retries = retries
|
50
|
+
super(app)
|
51
|
+
end
|
52
|
+
|
53
|
+
def call(env)
|
54
|
+
retries = @retries
|
55
|
+
begin
|
56
|
+
@app.call(env)
|
57
|
+
rescue StandardError, Timeout::Error
|
58
|
+
if retries > 0
|
59
|
+
retries -= 1
|
60
|
+
retry
|
61
|
+
end
|
62
|
+
raise
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request/token_authentication.rb
|
69
|
+
module Faraday
|
70
|
+
class Request::TokenAuthentication < Faraday::Middleware
|
71
|
+
def initialize(app, token, options={})
|
72
|
+
super(app)
|
73
|
+
|
74
|
+
# values = ["token=#{token.to_s.inspect}"]
|
75
|
+
# options.each do |key, value|
|
76
|
+
# values << "#{key}=#{value.to_s.inspect}"
|
77
|
+
# end
|
78
|
+
# comma = ",\n#{' ' * ('Authorization: Token '.size)}"
|
79
|
+
# @header_value = "Token #{values * comma}"
|
80
|
+
@header_value = "token #{token}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def call(env)
|
84
|
+
unless env[:request_headers]['Authorization']
|
85
|
+
env[:request_headers]['Authorization'] = @header_value
|
86
|
+
end
|
87
|
+
@app.call(env)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request.rb
|
93
|
+
Faraday::Request.register_lookup_modules \
|
94
|
+
:url_encoded => :UrlEncoded,
|
95
|
+
:multipart => :Multipart,
|
96
|
+
:retry => :Retry,
|
97
|
+
:basic_auth => :BasicAuthentication,
|
98
|
+
:token_auth => :TokenAuthentication
|
99
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: A Wrapper class that deals with normalizing Github responses.
|
6
|
+
class Normalizer < Wrapper
|
7
|
+
# Public: Fetches and normalizes a github entity.
|
8
|
+
#
|
9
|
+
# Returns normalized Response.
|
10
|
+
def [](key)
|
11
|
+
result = super
|
12
|
+
links(result)['self'] ||= { 'href' => full_url(key).to_s } if result.hash?
|
13
|
+
result
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def modify(*)
|
19
|
+
normalize super
|
20
|
+
end
|
21
|
+
|
22
|
+
def links(hash)
|
23
|
+
hash = hash.data if hash.respond_to? :data
|
24
|
+
hash["_links"] ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_link(hash, type, href)
|
28
|
+
links(hash)[type] = {"href" => href}
|
29
|
+
end
|
30
|
+
|
31
|
+
def normalize_response(response)
|
32
|
+
response = response.dup
|
33
|
+
response.data = normalize response.data
|
34
|
+
response
|
35
|
+
end
|
36
|
+
|
37
|
+
def normalize_hash(hash)
|
38
|
+
corrected = {}
|
39
|
+
corrected.default_proc = hash.default_proc if hash.default_proc
|
40
|
+
|
41
|
+
hash.each_pair do |key, value|
|
42
|
+
key = normalize_key(key, value)
|
43
|
+
next if normalize_url(corrected, key, value)
|
44
|
+
next if normalize_time(corrected, key, value)
|
45
|
+
corrected[key] = normalize(value)
|
46
|
+
end
|
47
|
+
|
48
|
+
normalize_user(corrected)
|
49
|
+
corrected
|
50
|
+
end
|
51
|
+
|
52
|
+
def normalize_time(hash, key, value)
|
53
|
+
hash['date'] = Time.at(value).xmlschema if key == 'timestamp'
|
54
|
+
end
|
55
|
+
|
56
|
+
def normalize_user(hash)
|
57
|
+
hash['owner'] ||= hash.delete('user') if hash['created_at'] and hash['user']
|
58
|
+
hash['author'] ||= hash.delete('user') if hash['committed_at'] and hash['user']
|
59
|
+
|
60
|
+
hash['committer'] ||= hash['author'] if hash['author']
|
61
|
+
hash['author'] ||= hash['committer'] if hash['committer']
|
62
|
+
end
|
63
|
+
|
64
|
+
def normalize_url(hash, key, value)
|
65
|
+
case key
|
66
|
+
when "blog"
|
67
|
+
set_link(hash, key, value)
|
68
|
+
when "url"
|
69
|
+
type = Addressable::URI.parse(value).host == api_host.host ? "self" : "html"
|
70
|
+
set_link(hash, type, value)
|
71
|
+
when /^(.+)_url$/
|
72
|
+
set_link(hash, $1, value)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def normalize_key(key, value = nil)
|
77
|
+
case key
|
78
|
+
when 'gravatar_url' then 'avatar_url'
|
79
|
+
when 'org' then 'organization'
|
80
|
+
when 'orgs' then 'organizations'
|
81
|
+
when 'username' then 'login'
|
82
|
+
when 'repo' then 'repository'
|
83
|
+
when 'repos' then normalize_key('repositories', value)
|
84
|
+
when /^repos?_(.*)$/ then "repository_#{$1}"
|
85
|
+
when /^(.*)_repo$/ then "#{$1}_repository"
|
86
|
+
when /^(.*)_repos$/ then "#{$1}_repositories"
|
87
|
+
when 'commit', 'commit_id' then value =~ /^\w{40}$/ ? 'sha' : key
|
88
|
+
when 'comments' then Numeric === value ? 'comment_count' : key
|
89
|
+
when 'forks' then Numeric === value ? 'fork_count' : key
|
90
|
+
when 'repositories' then Numeric === value ? 'repository_count' : key
|
91
|
+
when /^(.*)s_count$/ then "#{$1}_count"
|
92
|
+
else key
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def normalize_array(array)
|
97
|
+
array.map { |e| normalize(e) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def normalize(object)
|
101
|
+
case object
|
102
|
+
when Hash then normalize_hash(object)
|
103
|
+
when Array then normalize_array(object)
|
104
|
+
when Response then normalize_response(object)
|
105
|
+
else object
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/gh/remote.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'gh/faraday'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: This class deals with HTTP requests to Github. It is the base Wrapper you always want to use.
|
6
|
+
# Note that it is usually used implicitely by other wrapper classes if not specified.
|
7
|
+
class Remote < Wrapper
|
8
|
+
attr_reader :api_host, :connection, :headers
|
9
|
+
|
10
|
+
# Public: Generates a new Rempte instance.
|
11
|
+
#
|
12
|
+
# api_host - HTTP host to send requests to, has to include schema (https or http)
|
13
|
+
# options - Hash with configuration options:
|
14
|
+
# :token - OAuth token to use (optional).
|
15
|
+
# :username - Github user used for login (optional).
|
16
|
+
# :password - Github password used for login (optional).
|
17
|
+
# :origin - Value of the origin request header (optional).
|
18
|
+
# :headers - HTTP headers to be send on every request (optional).
|
19
|
+
# :adapter - HTTP library to use for making requests (optional, default: :net_http)
|
20
|
+
#
|
21
|
+
# It is highly recommended to set origin, but not to set headers.
|
22
|
+
# If you set the username, you should also set the password.
|
23
|
+
def setup(api_host, options)
|
24
|
+
token, username, password = options.values_at :token, :username, :password
|
25
|
+
|
26
|
+
api_host = api_host.api_host if api_host.respond_to? :api_host
|
27
|
+
@api_host = Addressable::URI.parse(api_host)
|
28
|
+
@headers = options[:headers].try(:dup) || {
|
29
|
+
"Origin" => options[:origin] || "http://example.org",
|
30
|
+
"Accept" => "application/vnd.github.v3.raw+json," \
|
31
|
+
"application/vnd.github.beta.raw+json;q=0.5," \
|
32
|
+
"application/json;q=0.1",
|
33
|
+
"Accept-Charset" => "utf-8"
|
34
|
+
}
|
35
|
+
|
36
|
+
@connection = Faraday.new(:url => api_host) do |builder|
|
37
|
+
builder.request(:token_auth, token) if token
|
38
|
+
builder.request(:basic_auth, username, password) if username and password
|
39
|
+
builder.request(:retry)
|
40
|
+
builder.response(:raise_error)
|
41
|
+
builder.adapter(options[:adapter] || :net_http)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: ...
|
46
|
+
def inspect
|
47
|
+
"#<#{self.class}: #{api_host}>"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: Retrieves resources from Github.
|
51
|
+
#
|
52
|
+
# Examples
|
53
|
+
#
|
54
|
+
# Github::Remote.new['users/rkh'] # => { ... }
|
55
|
+
#
|
56
|
+
# Raises Faraday::Error::ResourceNotFound if the resource returns status 404.
|
57
|
+
# Raises Faraday::Error::ClientError if the resource returns a status between 400 and 599.
|
58
|
+
# Returns the Response.
|
59
|
+
def [](key)
|
60
|
+
response = connection.get(path_for(key), headers)
|
61
|
+
modify(response.body, response.headers)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def modify(body, headers = {})
|
67
|
+
return body if body.is_a? Response
|
68
|
+
Response.new(headers, body)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/gh/response.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: Class wrapping low level Github responses.
|
6
|
+
#
|
7
|
+
# Delegates safe methods to the parsed body (expected to be an Array or Hash).
|
8
|
+
class Response
|
9
|
+
include GH::Case
|
10
|
+
|
11
|
+
# Internal: Content-Type header value expected from Github
|
12
|
+
CONTENT_TYPE = "application/json; charset=utf-8"
|
13
|
+
|
14
|
+
include Enumerable
|
15
|
+
attr_accessor :headers, :data, :body
|
16
|
+
|
17
|
+
# subset of safe methods that both Array and Hash implement
|
18
|
+
extend Forwardable
|
19
|
+
def_delegators(:@data, :[], :assoc, :each, :empty?, :flatten, :include?, :index, :inspect, :length,
|
20
|
+
:pretty_print, :pretty_print_cycle, :rassoc, :select, :size, :to_a, :values_at)
|
21
|
+
|
22
|
+
# Internal: Initializes a new instance.
|
23
|
+
#
|
24
|
+
# headers - HTTP headers as a Hash
|
25
|
+
# body - HTTP body as a String
|
26
|
+
def initialize(headers = {}, body = "{}")
|
27
|
+
@headers = Hash[headers.map { |k,v| [k.downcase, v] }]
|
28
|
+
raise ArgumentError, "unexpected Content-Type #{content_type}" if content_type and content_type != CONTENT_TYPE
|
29
|
+
|
30
|
+
case body
|
31
|
+
when respond_to(:to_str) then @body = body.to_str
|
32
|
+
when respond_to(:to_hash) then @data = body.to_hash
|
33
|
+
when respond_to(:to_ary) then @data = body.to_ary
|
34
|
+
else raise ArgumentError, "cannot parse #{body.inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
@body ||= MultiJson.encode(@data)
|
38
|
+
@body = @body.encode("utf-8") if @body.respond_to? :encode
|
39
|
+
@data ||= MultiJson.decode(@body)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Duplicates the instance. Will also duplicate some instance variables to behave as expected.
|
43
|
+
#
|
44
|
+
# Returns new Response instance.
|
45
|
+
def dup
|
46
|
+
super.dup_ivars
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Returns the response body as a String.
|
50
|
+
def to_s
|
51
|
+
@body.dup
|
52
|
+
end
|
53
|
+
|
54
|
+
# Public: Returns true or false indicating whether it supports method.
|
55
|
+
def respond_to?(method, *)
|
56
|
+
return super unless method.to_s == "to_hash" or method.to_s == "to_ary"
|
57
|
+
data.respond_to? method
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: Implements to_hash conventions, please check respond_to?(:to_hash).
|
61
|
+
def to_hash
|
62
|
+
return method_missing(__method__) unless respond_to? __method__
|
63
|
+
@data.dup.to_hash
|
64
|
+
end
|
65
|
+
|
66
|
+
# Public: Implements to_ary conventions, please check respond_to?(:to_hash).
|
67
|
+
def to_ary
|
68
|
+
return method_missing(__method__) unless respond_to? __method__
|
69
|
+
@data.dup.to_ary
|
70
|
+
end
|
71
|
+
|
72
|
+
def hash?
|
73
|
+
respond_to? :to_hash
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def dup_ivars
|
79
|
+
@headers, @data, @body = @headers.dup, @data.dup, @body.dup
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def content_type
|
86
|
+
headers['content-type']
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/gh/stack.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'gh'
|
2
|
+
|
3
|
+
module GH
|
4
|
+
# Public: Exposes DSL for stacking wrappers.
|
5
|
+
#
|
6
|
+
# Examples
|
7
|
+
#
|
8
|
+
# api = GH::Stack.build do
|
9
|
+
# use GH::Cache, cache: Rails.cache
|
10
|
+
# use GH::Normalizer
|
11
|
+
# use GH::Remote, username: "admin", password: "admin"
|
12
|
+
# end
|
13
|
+
class Stack
|
14
|
+
# Public: Generates a new wrapper stack from the given block.
|
15
|
+
#
|
16
|
+
# options - Hash of options that will be passed to all layers upon initialization.
|
17
|
+
#
|
18
|
+
# Returns top most Wrapper instance.
|
19
|
+
def self.build(options, &block)
|
20
|
+
new(&block).build(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Public: Generates a new Stack instance.
|
24
|
+
#
|
25
|
+
# options - Hash of options that will be passed to all layers upon initialization.
|
26
|
+
#
|
27
|
+
# Can be used for easly stacking layers.
|
28
|
+
def initialize(options = {}, &block)
|
29
|
+
@options, @stack = {}, []
|
30
|
+
instance_eval(&block) if block
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Adds a new layer to the stack.
|
34
|
+
#
|
35
|
+
# Layer will be wrapped by layers already on the stack.
|
36
|
+
def use(klass, options = {})
|
37
|
+
@stack << [klass, options]
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Generates wrapper instances for stack configuration.
|
42
|
+
#
|
43
|
+
# options - Hash of options that will be passed to all layers upon initialization.
|
44
|
+
#
|
45
|
+
# Returns top most Wrapper instance.
|
46
|
+
def build(options = {})
|
47
|
+
@stack.reverse.inject(nil) do |backend, (klass, opts)|
|
48
|
+
klass.new backend, @options.merge(opts).merge(options)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/gh/version.rb
ADDED
data/lib/gh/wrapper.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'addressable/uri'
|
3
|
+
|
4
|
+
module GH
|
5
|
+
# Public: Simple base class for low level layers.
|
6
|
+
# Handy if you want to manipulate resources coming in from Github.
|
7
|
+
#
|
8
|
+
# Examples
|
9
|
+
#
|
10
|
+
# class IndifferentAccess
|
11
|
+
# def [](key) super.tap { |r| r.data.with_indifferent_access! } end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# gh = IndifferentAccess.new
|
15
|
+
# gh['users/rkh'][:name] # => "Konstantin Haase"
|
16
|
+
#
|
17
|
+
# # easy to use in the low level stack
|
18
|
+
# gh = Github.build do
|
19
|
+
# use GH::Cache
|
20
|
+
# use IndifferentAccess
|
21
|
+
# use GH::Normalizer
|
22
|
+
# end
|
23
|
+
class Wrapper
|
24
|
+
extend Forwardable
|
25
|
+
|
26
|
+
# Public: Get wrapped layer.
|
27
|
+
attr_reader :backend
|
28
|
+
|
29
|
+
# Public: Returns the URI used for sending out web request.
|
30
|
+
def_delegator :backend, :api_host
|
31
|
+
|
32
|
+
# Public: Retrieves resources from Github.
|
33
|
+
#
|
34
|
+
# By default, this method is delegated to the next layer on the stack
|
35
|
+
# and modify is called.
|
36
|
+
def [](key)
|
37
|
+
modify backend[key]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Internal: Get/set default layer to wrap when creating a new instance.
|
41
|
+
def self.wraps(klass = nil)
|
42
|
+
@wraps = klass if klass
|
43
|
+
@wraps || Remote
|
44
|
+
end
|
45
|
+
|
46
|
+
# Public: Initialize a new Wrapper.
|
47
|
+
#
|
48
|
+
# backend - layer to be wrapped
|
49
|
+
# options - config options
|
50
|
+
def initialize(backend = nil, options = {})
|
51
|
+
setup(*normalize_options(backend, options))
|
52
|
+
options.each_pair { |key, value| public_send("#{key}=", value) if respond_to? "#{key}=" }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Set wrapped layer.
|
56
|
+
def backend=(layer)
|
57
|
+
layer.frontend = self
|
58
|
+
@backend = layer
|
59
|
+
end
|
60
|
+
|
61
|
+
# Internal: ...
|
62
|
+
def frontend=(value)
|
63
|
+
@frontend = value
|
64
|
+
end
|
65
|
+
|
66
|
+
# Internal: ...
|
67
|
+
def frontend
|
68
|
+
@frontend ? @frontend.frontend : self
|
69
|
+
end
|
70
|
+
|
71
|
+
def inspect
|
72
|
+
"#<#{self.class}: #{backend.inspect}>"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def modify(data)
|
78
|
+
data
|
79
|
+
end
|
80
|
+
|
81
|
+
def setup(backend, options)
|
82
|
+
self.backend = Wrapper === backend ? backend : self.class.wraps.new(backend, options)
|
83
|
+
end
|
84
|
+
|
85
|
+
def normalize_options(backend, options)
|
86
|
+
backend, options = nil, backend if Hash === backend
|
87
|
+
options ||= {}
|
88
|
+
backend ||= options[:backend] || options[:api_url] || 'https://api.github.com'
|
89
|
+
[backend, options]
|
90
|
+
end
|
91
|
+
|
92
|
+
def full_url(key)
|
93
|
+
api_host + path_for(key)
|
94
|
+
end
|
95
|
+
|
96
|
+
def path_for(key)
|
97
|
+
uri = Addressable::URI.parse(key)
|
98
|
+
raise ArgumentError, "URI out of scope: #{key}" if uri.host and uri.host != api_host.host
|
99
|
+
uri.request_uri
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/spec/cache_spec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GH::Cache do
|
4
|
+
before { subject.backend = GH::MockBackend.new }
|
5
|
+
|
6
|
+
it 'send HTTP requests for uncached resources' do
|
7
|
+
subject['users/rkh']['name'].should be == "Konstantin Haase"
|
8
|
+
requests.count.should be == 1
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'uses the cache for subsequent requests' do
|
12
|
+
subject['users/rkh']['name'].should be == "Konstantin Haase"
|
13
|
+
subject['users/svenfuchs']['name'].should be == "Sven Fuchs"
|
14
|
+
subject['users/rkh']['name'].should be == "Konstantin Haase"
|
15
|
+
requests.count.should be == 2
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GH::Normalizer do
|
4
|
+
before { subject.backend = GH::MockBackend.new }
|
5
|
+
|
6
|
+
def normalize(payload)
|
7
|
+
data['payload'] = payload
|
8
|
+
end
|
9
|
+
|
10
|
+
def with_headers(headers = {})
|
11
|
+
response = GH::Response.new(headers)
|
12
|
+
data['payload'], response.data = response, data['payload']
|
13
|
+
end
|
14
|
+
|
15
|
+
def normalized
|
16
|
+
subject['payload']
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'is set up properly' do
|
20
|
+
backend.frontend.should be_a(GH::Normalizer)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'leaves unknown fields in place' do
|
24
|
+
normalize 'foo' => 'bar'
|
25
|
+
normalized['foo'].should be == 'bar'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'works for deeply nested fields'
|
29
|
+
it 'works for lists'
|
30
|
+
|
31
|
+
context 'date fields' do
|
32
|
+
it 'generates date from timestamp'
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'renaming' do
|
36
|
+
def self.renames(a, b)
|
37
|
+
it "renames #{a} to #{b}" do
|
38
|
+
normalize a => "foo"
|
39
|
+
normalized.should_not include(a)
|
40
|
+
normalized.should include(b)
|
41
|
+
normalized[b].should be == "foo"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
renames 'org', 'organization'
|
46
|
+
renames 'orgs', 'organizations'
|
47
|
+
renames 'username', 'login'
|
48
|
+
renames 'repo', 'repository'
|
49
|
+
renames 'repos', 'repositories'
|
50
|
+
renames 'repo_foo', 'repository_foo'
|
51
|
+
renames 'repos_foo', 'repository_foo'
|
52
|
+
renames 'foo_repo', 'foo_repository'
|
53
|
+
renames 'foo_repos', 'foo_repositories'
|
54
|
+
|
55
|
+
it 'renames commit to sha if value is a sha' do
|
56
|
+
normalize 'commit' => 'd0f4aa01f100c26c6eae17ea637f46cf150d9c1f'
|
57
|
+
normalized.should_not include('commit')
|
58
|
+
normalized.should include('sha')
|
59
|
+
normalized['sha'].should be == 'd0f4aa01f100c26c6eae17ea637f46cf150d9c1f'
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'does not rename commit to sha if value is not a sha' do
|
63
|
+
normalize 'commit' => 'foo'
|
64
|
+
normalized.should include('commit')
|
65
|
+
normalized.should_not include('sha')
|
66
|
+
normalized['commit'].should be == 'foo'
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'renames commit_id to sha if value is a sha' do
|
70
|
+
normalize 'commit_id' => 'd0f4aa01f100c26c6eae17ea637f46cf150d9c1f'
|
71
|
+
normalized.should_not include('commit_id')
|
72
|
+
normalized.should include('sha')
|
73
|
+
normalized['sha'].should be == 'd0f4aa01f100c26c6eae17ea637f46cf150d9c1f'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'does not rename commit_id to sha if value is not a sha' do
|
77
|
+
normalize 'commit_id' => 'foo'
|
78
|
+
normalized.should include('commit_id')
|
79
|
+
normalized.should_not include('sha')
|
80
|
+
normalized['commit_id'].should be == 'foo'
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'renames comments to comment_count if content is a number' do
|
84
|
+
normalize 'comments' => 42
|
85
|
+
normalized.should include('comment_count')
|
86
|
+
normalized.should_not include('comments')
|
87
|
+
normalized['comment_count'].should be == 42
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'renames repositories to repository_count if content is a number' do
|
91
|
+
normalize 'repositories' => 42
|
92
|
+
normalized.should include('repository_count')
|
93
|
+
normalized.should_not include('repositories')
|
94
|
+
normalized['repository_count'].should be == 42
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'renames repos to repository_count if content is a number' do
|
98
|
+
normalize 'repos' => 42
|
99
|
+
normalized.should include('repository_count')
|
100
|
+
normalized.should_not include('repos')
|
101
|
+
normalized['repository_count'].should be == 42
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'renames forks to fork_count if content is a number' do
|
105
|
+
normalize 'forks' => 42
|
106
|
+
normalized.should include('fork_count')
|
107
|
+
normalized.should_not include('forks')
|
108
|
+
normalized['fork_count'].should be == 42
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'does not rename comments to comment_count if content is not a number' do
|
112
|
+
normalize 'comments' => 'foo'
|
113
|
+
normalized.should include('comments')
|
114
|
+
normalized.should_not include('comment_count')
|
115
|
+
normalized['comments'].should be == 'foo'
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'does not rename repositories to repository_count if content is not a number' do
|
119
|
+
normalize 'repositories' => 'foo'
|
120
|
+
normalized.should include('repositories')
|
121
|
+
normalized.should_not include('repository_count')
|
122
|
+
normalized['repositories'].should be == 'foo'
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'does not rename repos to repository_count if content is not a number' do
|
126
|
+
normalize 'repos' => 'foo'
|
127
|
+
normalized.should include('repositories')
|
128
|
+
normalized.should_not include('repository_count')
|
129
|
+
normalized['repositories'].should be == 'foo'
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'does not rename forks to fork_count if content is not a number' do
|
133
|
+
normalize 'forks' => 'foo'
|
134
|
+
normalized.should include('forks')
|
135
|
+
normalized.should_not include('fork_count')
|
136
|
+
normalized['forks'].should be == 'foo'
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'renames user to owner if appropriate' do
|
140
|
+
normalize 'user' => 'me', 'created_at' => Time.now.xmlschema
|
141
|
+
normalized.should_not include('user')
|
142
|
+
normalized.should include('owner')
|
143
|
+
normalized['owner'].should be == 'me'
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'renames user to author if appropriate' do
|
147
|
+
normalize 'user' => 'me', 'committed_at' => Time.now.xmlschema
|
148
|
+
normalized.should_not include('user')
|
149
|
+
normalized.should include('author')
|
150
|
+
normalized['author'].should be == 'me'
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'leaves user in place if owner exists' do
|
154
|
+
normalize 'user' => 'me', 'created_at' => Time.now.xmlschema, 'owner' => 'you'
|
155
|
+
normalized.should include('user')
|
156
|
+
normalized.should include('owner')
|
157
|
+
normalized['user'].should be == 'me'
|
158
|
+
normalized['owner'].should be == 'you'
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'leaves user in place if author exists' do
|
162
|
+
normalize 'user' => 'me', 'committed_at' => Time.now.xmlschema, 'author' => 'you'
|
163
|
+
normalized.should include('user')
|
164
|
+
normalized.should include('author')
|
165
|
+
normalized['user'].should be == 'me'
|
166
|
+
normalized['author'].should be == 'you'
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'leaves user in place if no indication what kind of user' do
|
170
|
+
normalize 'user' => 'me'
|
171
|
+
normalized.should_not include('owner')
|
172
|
+
normalized.should_not include('author')
|
173
|
+
normalized.should include('user')
|
174
|
+
normalized['user'].should be == 'me'
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'copies author to committer' do
|
178
|
+
normalize 'author' => 'me'
|
179
|
+
normalized.should include('author')
|
180
|
+
normalized.should include('committer')
|
181
|
+
normalized['author'].should be == 'me'
|
182
|
+
normalized['author'].should be_equal(normalized['committer'])
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'copies committer to author' do
|
186
|
+
normalize 'committer' => 'me'
|
187
|
+
normalized.should include('author')
|
188
|
+
normalized.should include('committer')
|
189
|
+
normalized['author'].should be == 'me'
|
190
|
+
normalized['author'].should be_equal(normalized['committer'])
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'does not override committer or author if both exist' do
|
194
|
+
normalize 'committer' => 'me', 'author' => 'you'
|
195
|
+
normalized.should include('author')
|
196
|
+
normalized.should include('committer')
|
197
|
+
normalized['author'].should be == 'you'
|
198
|
+
normalized['committer'].should be == 'me'
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context 'links' do
|
203
|
+
it 'generates link entries from link headers' do
|
204
|
+
pending
|
205
|
+
normalize '_links' => {'href' => 'foo'}
|
206
|
+
with_headers
|
207
|
+
|
208
|
+
normalized.headers.should include("Link")
|
209
|
+
normalized.headers["Link"].should be == "something something"
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'generates link headers from link entries'
|
213
|
+
it 'does not discard existing link entires'
|
214
|
+
it 'does not discard existing link headers'
|
215
|
+
|
216
|
+
it 'identifies _url suffix as link' do
|
217
|
+
normalize 'foo_url' => 'http://lmgtfy.com/?q=foo'
|
218
|
+
normalized.should_not include('foo_url')
|
219
|
+
normalized.should include("_links")
|
220
|
+
normalized["_links"].should include("foo")
|
221
|
+
normalized["_links"]["foo"].should be_a(Hash)
|
222
|
+
normalized["_links"]["foo"]["href"].should be == 'http://lmgtfy.com/?q=foo'
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'identifies blog as link' do
|
226
|
+
normalize 'blog' => 'http://rkh.im'
|
227
|
+
normalized.should_not include('blog')
|
228
|
+
normalized.should include("_links")
|
229
|
+
normalized["_links"].should include("blog")
|
230
|
+
normalized["_links"]["blog"].should be_a(Hash)
|
231
|
+
normalized["_links"]["blog"]["href"].should be == 'http://rkh.im'
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'detects avatar links from gravatar_url' do
|
235
|
+
normalize 'gravatar_url' => 'http://gravatar.com/avatar/93c02710978db9979064630900741691?size=50'
|
236
|
+
normalized.should_not include('gravatar_url')
|
237
|
+
normalized.should include("_links")
|
238
|
+
normalized["_links"].should include("avatar")
|
239
|
+
normalized["_links"]["avatar"].should be_a(Hash)
|
240
|
+
normalized["_links"]["avatar"]["href"].should be == 'http://gravatar.com/avatar/93c02710978db9979064630900741691?size=50'
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'detects html urls in url field' do
|
244
|
+
normalize 'url' => 'http://github.com/foo'
|
245
|
+
normalized.should_not include('url')
|
246
|
+
normalized.should include('_links')
|
247
|
+
normalized['_links'].should include('html')
|
248
|
+
normalized['_links']['html']['href'].should be == 'http://github.com/foo'
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'detects self urls in url field' do
|
252
|
+
normalize 'url' => 'http://api.github.com/foo'
|
253
|
+
normalized.should_not include('url')
|
254
|
+
normalized.should include('_links')
|
255
|
+
normalized['_links'].should include('self')
|
256
|
+
normalized['_links'].should_not include('html')
|
257
|
+
normalized['_links']['self']['href'].should be == 'http://api.github.com/foo'
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
---
|
2
|
+
- !binary "c2VydmVy": !binary |-
|
3
|
+
bmdpbngvMS4wLjEy
|
4
|
+
!binary "ZGF0ZQ==": !binary |-
|
5
|
+
VHVlLCAwNiBNYXIgMjAxMiAxNjo1MTozNyBHTVQ=
|
6
|
+
!binary "Y29udGVudC10eXBl": !binary |-
|
7
|
+
YXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOA==
|
8
|
+
!binary "dHJhbnNmZXItZW5jb2Rpbmc=": !binary |-
|
9
|
+
Y2h1bmtlZA==
|
10
|
+
!binary "Y29ubmVjdGlvbg==": !binary |-
|
11
|
+
a2VlcC1hbGl2ZQ==
|
12
|
+
!binary "c3RhdHVz": !binary |-
|
13
|
+
MjAwIE9L
|
14
|
+
!binary "eC1yYXRlbGltaXQtbGltaXQ=": !binary |-
|
15
|
+
NTAwMA==
|
16
|
+
!binary "ZXRhZw==": !binary |-
|
17
|
+
IjMzYjg0NTM3MTBjZjQ2OWQ1MGE5ZjJhNWM3MWM1YmU1Ig==
|
18
|
+
!binary "eC1yYXRlbGltaXQtcmVtYWluaW5n": !binary |-
|
19
|
+
NDk5NQ==
|
20
|
+
- ! '{"type":"User","following":485,"login":"rkh","public_repos":117,"public_gists":219,"html_url":"https://github.com/rkh","blog":"http://rkh.im","hireable":false,"bio":"","location":"Potsdam,
|
21
|
+
Berlin, Portland","company":"Travis CI","followers":401,"url":"https://api.github.com/users/rkh","created_at":"2008-10-22T18:56:03Z","name":"Konstantin
|
22
|
+
Haase","email":"k.haase@finn.de","gravatar_id":"5c2b452f6eea4a6d84c105ebd971d2a4","id":30442,"avatar_url":"https://secure.gravatar.com/avatar/5c2b452f6eea4a6d84c105ebd971d2a4?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png"}'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
- !binary "c2VydmVy": !binary |-
|
3
|
+
bmdpbngvMS4wLjEy
|
4
|
+
!binary "ZGF0ZQ==": !binary |-
|
5
|
+
V2VkLCAwNyBNYXIgMjAxMiAxNDo0NDowOCBHTVQ=
|
6
|
+
!binary "Y29udGVudC10eXBl": !binary |-
|
7
|
+
YXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOA==
|
8
|
+
!binary "dHJhbnNmZXItZW5jb2Rpbmc=": !binary |-
|
9
|
+
Y2h1bmtlZA==
|
10
|
+
!binary "Y29ubmVjdGlvbg==": !binary |-
|
11
|
+
a2VlcC1hbGl2ZQ==
|
12
|
+
!binary "c3RhdHVz": !binary |-
|
13
|
+
MjAwIE9L
|
14
|
+
!binary "eC1yYXRlbGltaXQtbGltaXQ=": !binary |-
|
15
|
+
NTAwMA==
|
16
|
+
!binary "ZXRhZw==": !binary |-
|
17
|
+
IjgwZjgyNzk4N2UwNGNmZjcxYWZkMjM4YjhjY2ExNjcwIg==
|
18
|
+
!binary "eC1yYXRlbGltaXQtcmVtYWluaW5n": !binary |-
|
19
|
+
NDk5OQ==
|
20
|
+
- ! '{"html_url":"https://github.com/svenfuchs","type":"User","hireable":false,"following":90,"login":"svenfuchs","bio":"","avatar_url":"https://secure.gravatar.com/avatar/402602a60e500e85f2f5dc1ff3648ecb?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","public_repos":86,"blog":"http://svenfuchs.com","location":"Germany/Berlin","company":null,"followers":272,"url":"https://api.github.com/users/svenfuchs","created_at":"2008-03-04T20:38:09Z","name":"Sven
|
21
|
+
Fuchs","email":"me@svenfuchs.com","gravatar_id":"402602a60e500e85f2f5dc1ff3648ecb","id":2208,"public_gists":77}'
|
data/spec/remote_spec.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GH::Remote do
|
4
|
+
it 'loads resources from github' do
|
5
|
+
stub_request(:get, "https://api.github.com/foo").to_return(:body => '["foo"]')
|
6
|
+
subject['foo'].to_s.should be == '["foo"]'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'sets headers correctly' do
|
10
|
+
stub_request(:get, "https://api.github.com/foo").to_return(:headers => {'X-Foo' => 'bar'}, :body => '[]')
|
11
|
+
subject['foo'].headers['x-foo'].should be == 'bar'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'raises an exception for missing resources' do
|
15
|
+
stub_request(:get, "https://api.github.com/foo").to_return(:status => 404)
|
16
|
+
expect { subject['foo'] }.to raise_error(Faraday::Error::ResourceNotFound)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'parses the body' do
|
20
|
+
stub_request(:get, "https://api.github.com/foo").to_return(:body => '{"foo":"bar"}')
|
21
|
+
subject['foo']['foo'].should be == 'bar'
|
22
|
+
end
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'gh'
|
2
|
+
require 'webmock/rspec'
|
3
|
+
require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module GH
|
7
|
+
module TestHelpers
|
8
|
+
def backend
|
9
|
+
subject.backend
|
10
|
+
end
|
11
|
+
|
12
|
+
def requests
|
13
|
+
backend.requests
|
14
|
+
end
|
15
|
+
|
16
|
+
def data
|
17
|
+
backend.data
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class MockBackend < Wrapper
|
22
|
+
attr_accessor :data, :requests
|
23
|
+
|
24
|
+
def setup(*)
|
25
|
+
@data, @requests = {}, []
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
key = path_for(key)
|
31
|
+
file = File.expand_path("../payloads/#{key}.yml", __FILE__)
|
32
|
+
@requests << key
|
33
|
+
|
34
|
+
result = @data[key] ||= begin
|
35
|
+
unless File.exist? file
|
36
|
+
res = allow_http { super }
|
37
|
+
FileUtils.mkdir_p File.dirname(file)
|
38
|
+
File.write file, [res.headers, res.body].to_yaml
|
39
|
+
end
|
40
|
+
|
41
|
+
Response.new(*YAML.load_file(file))
|
42
|
+
end
|
43
|
+
|
44
|
+
result = Response.new({}, result) unless result.is_a? Response
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def allow_http
|
51
|
+
raise Faraday::Error::ResourceNotFound if ENV['CI']
|
52
|
+
WebMock.allow_net_connect!
|
53
|
+
yield
|
54
|
+
ensure
|
55
|
+
WebMock.disable_net_connect!
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
RSpec.configure do |c|
|
61
|
+
c.include GH::TestHelpers
|
62
|
+
end
|
data/spec/stack_spec.rb
ADDED
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Konstantin Haase
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70328844248000 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70328844248000
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: webmock
|
27
|
+
requirement: &70328844247440 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70328844247440
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: faraday
|
38
|
+
requirement: &70328844262860 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0.7'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70328844262860
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: backports
|
49
|
+
requirement: &70328844262340 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.3'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70328844262340
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: multi_json
|
60
|
+
requirement: &70328844261880 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '1.0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70328844261880
|
69
|
+
description: multi-layer client for the github api v3
|
70
|
+
email:
|
71
|
+
- konstantin.mailinglists@googlemail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- .travis.yml
|
78
|
+
- Gemfile
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- gh.gemspec
|
82
|
+
- lib/gh.rb
|
83
|
+
- lib/gh/cache.rb
|
84
|
+
- lib/gh/case.rb
|
85
|
+
- lib/gh/faraday.rb
|
86
|
+
- lib/gh/normalizer.rb
|
87
|
+
- lib/gh/remote.rb
|
88
|
+
- lib/gh/response.rb
|
89
|
+
- lib/gh/stack.rb
|
90
|
+
- lib/gh/version.rb
|
91
|
+
- lib/gh/wrapper.rb
|
92
|
+
- spec/cache_spec.rb
|
93
|
+
- spec/normalizer_spec.rb
|
94
|
+
- spec/payloads/users/rkh.yml
|
95
|
+
- spec/payloads/users/svenfuchs.yml
|
96
|
+
- spec/remote_spec.rb
|
97
|
+
- spec/response_spec.rb
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
- spec/stack_spec.rb
|
100
|
+
- spec/wrapper_spec.rb
|
101
|
+
homepage: ''
|
102
|
+
licenses: []
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 1.8.11
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: layered github client
|
125
|
+
test_files:
|
126
|
+
- spec/cache_spec.rb
|
127
|
+
- spec/normalizer_spec.rb
|
128
|
+
- spec/payloads/users/rkh.yml
|
129
|
+
- spec/payloads/users/svenfuchs.yml
|
130
|
+
- spec/remote_spec.rb
|
131
|
+
- spec/response_spec.rb
|
132
|
+
- spec/spec_helper.rb
|
133
|
+
- spec/stack_spec.rb
|
134
|
+
- spec/wrapper_spec.rb
|