gh 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|