gh 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - jruby-18mode
7
+ - jruby-19mode
8
+ - rbx-18mode
9
+ - rbx-19mode
10
+ - ruby-head
11
+ - jruby-head
12
+ - ree
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in gh.gemspec
4
+ gemspec
5
+ gem 'rake'
6
+ platform(:jruby) { gem 'jruby-openssl' }
@@ -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
+ ```
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new :default do |t|
5
+ t.rspec_opts = %w[-bcfd --fail-fast]
6
+ end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'gh'
2
+
3
+ module GH
4
+ module Case
5
+ def respond_to(method) proc { |o| o.respond_to? method } end
6
+ private :respond_to
7
+ end
8
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ module GH
2
+ # Public: Library version.
3
+ VERSION = "0.0.1"
4
+ end
@@ -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
@@ -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}'
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe GH::Response do
4
+ it 'is specified'
5
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe GH::Stack do
4
+ it 'is specified'
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe GH::Wrapper do
4
+ it 'is specified'
5
+ end
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