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