Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f302349
Showing
21 changed files
with
636 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
*.gem | ||
.bundle | ||
Gemfile.lock | ||
pkg/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
source "http://rubygems.org" | ||
|
||
# Specify your gem's dependencies in gh.gemspec | ||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
**This is work in progress and not yet usable!** | ||
|
||
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. | ||
|
||
With that in mind, this library follows the following goals: | ||
|
||
* Implement features in separate layers, make layers as independend of each other as possible | ||
* Higher level layers should not worry about when to send requests | ||
* It should only send requests to Github when necessary | ||
* 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) | ||
* It should be able to deal with events and hooks well (i.e. update cached entities with hook data) | ||
* It should not have intransparent magic (i.e. implicit, undocumented requirements on fields we get from Github) | ||
* It should shield against possible changes to the Github API or at least complain about those changes if it can't deal with it. | ||
|
||
Most of this is not yet implemented! | ||
|
||
The lower level APIs support a Rack-like stacking API: | ||
|
||
``` ruby | ||
api = GH::Stack.build do | ||
use GH::Cache, cache: Rails.cache | ||
use GH::Normalizer | ||
use GH::Remote, username: "admin", password: "admin" | ||
end | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require 'bundler/gem_tasks' | ||
require 'rspec/core/rake_task' | ||
RSpec::Core::RakeTask.new(:default) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# -*- encoding: utf-8 -*- | ||
$:.push File.expand_path("../lib", __FILE__) | ||
require "gh/version" | ||
|
||
Gem::Specification.new do |s| | ||
s.name = "gh" | ||
s.version = GH::VERSION | ||
s.authors = ["Konstantin Haase"] | ||
s.email = ["konstantin.mailinglists@googlemail.com"] | ||
s.homepage = "" | ||
s.summary = %q{layered github client} | ||
s.description = %q{multi-layer client for the github api v3} | ||
|
||
s.files = `git ls-files`.split("\n") | ||
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") | ||
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } | ||
s.require_paths = ["lib"] | ||
|
||
s.add_development_dependency "rspec" | ||
s.add_runtime_dependency 'faraday', '~> 0.7' | ||
s.add_runtime_dependency 'backports', '~> 2.3' | ||
s.add_runtime_dependency 'multi_json', '~> 1.0' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
require 'gh/version' | ||
require 'backports' | ||
require 'forwardable' | ||
|
||
module GH | ||
autoload :Cache, 'gh/cache' | ||
autoload :Normalizer, 'gh/normalizer' | ||
autoload :Remote, 'gh/remote' | ||
autoload :Response, 'gh/response' | ||
autoload :Stack, 'gh/stack' | ||
autoload :Wrapper, 'gh/wrapper' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
require 'gh' | ||
require 'thread' | ||
|
||
module GH | ||
# Public: This class deals with HTTP requests to Github. It is the base Wrapper you always want to use. | ||
# Note that it is usually used implicitely by other wrapper classes if not specified. | ||
class Cache < Wrapper | ||
# Public: Get/set cache to use. Compatible with Rails/ActiveSupport cache. | ||
attr_accessor :cache | ||
|
||
# Internal: Simple in-memory cache basically implementing a copying GC. | ||
class SimpleCache | ||
# Internal: Initializes a new SimpleCache. | ||
# | ||
# size - Number of objects to hold in cache. | ||
def initialize(size = 2048) | ||
@old, @new, @size, @mutex = {}, {}, size/2, Mutex.new | ||
end | ||
|
||
# Internal: Tries to fetch a value from the cache and if it doesn't exist, generates it from the | ||
# block given. | ||
def fetch(key) | ||
@mutex.lock { @old, @new = @new, {} if @new.size > @size } if @new.size > @size | ||
@new[key] ||= @old[key] || yield | ||
end | ||
end | ||
|
||
# Public: Initializes a new Cache instance. | ||
# | ||
# backend - Backend to wrap (defaults to Remote) | ||
# options - Configuration options: | ||
# :cache - Cache to be used. | ||
def initialize(*) | ||
super | ||
self.cache ||= Rails.cache if defined? Rails.cache | ||
self.cache ||= ActiveSupport::Cache.lookup_store if defined? ActiveSupport::Cache.lookup_store | ||
self.cache ||= SimpleCache.new | ||
end | ||
|
||
# Public: Retrieves resources from Github and caches response for future access. | ||
# | ||
# Examples | ||
# | ||
# Github::Cache.new['users/rkh'] # => { ... } | ||
# | ||
# Returns the Response. | ||
def [](key) | ||
cache.fetch(path_for(key)) { super } | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Copyright (c) 2009 rick olson, zack hobson | ||
# | ||
# Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
# this software and associated documentation files (the "Software"), to deal in | ||
# the Software without restriction, including without limitation the rights to | ||
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||
# of the Software, and to permit persons to whom the Software is furnished to do | ||
# so, subject to the following conditions: | ||
# | ||
# The above copyright notice and this permission notice shall be included in all | ||
# copies or substantial portions of the Software. | ||
# | ||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
# SOFTWARE. | ||
|
||
require 'faraday' | ||
|
||
if Faraday::VERSION < '0.8.0' | ||
$stderr.puts "please update faraday" | ||
|
||
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request/basic_authentication.rb | ||
require 'base64' | ||
|
||
module Faraday | ||
class Request::BasicAuthentication < Faraday::Middleware | ||
def initialize(app, login, pass) | ||
super(app) | ||
@header_value = "Basic #{Base64.encode64([login, pass].join(':')).gsub("\n", '')}" | ||
end | ||
|
||
def call(env) | ||
unless env[:request_headers]['Authorization'] | ||
env[:request_headers]['Authorization'] = @header_value | ||
end | ||
@app.call(env) | ||
end | ||
end | ||
end | ||
|
||
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request/retry.rb | ||
module Faraday | ||
class Request::Retry < Faraday::Middleware | ||
def initialize(app, retries = 2) | ||
@retries = retries | ||
super(app) | ||
end | ||
|
||
def call(env) | ||
retries = @retries | ||
begin | ||
@app.call(env) | ||
rescue StandardError, Timeout::Error | ||
if retries > 0 | ||
retries -= 1 | ||
retry | ||
end | ||
raise | ||
end | ||
end | ||
end | ||
end | ||
|
||
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request/token_authentication.rb | ||
module Faraday | ||
class Request::TokenAuthentication < Faraday::Middleware | ||
def initialize(app, token, options={}) | ||
super(app) | ||
|
||
values = ["token=#{token.to_s.inspect}"] | ||
options.each do |key, value| | ||
values << "#{key}=#{value.to_s.inspect}" | ||
end | ||
comma = ",\n#{' ' * ('Authorization: Token '.size)}" | ||
@header_value = "Token #{values * comma}" | ||
end | ||
|
||
def call(env) | ||
unless env[:request_headers]['Authorization'] | ||
env[:request_headers]['Authorization'] = @header_value | ||
end | ||
@app.call(env) | ||
end | ||
end | ||
end | ||
|
||
# https://github.com/technoweenie/faraday/blob/master/lib/faraday/request.rb | ||
Faraday::Request.register_lookup_modules \ | ||
:url_encoded => :UrlEncoded, | ||
:multipart => :Multipart, | ||
:retry => :Retry, | ||
:basic_auth => :BasicAuthentication, | ||
:token_auth => :TokenAuthentication | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
require 'gh' | ||
require 'time' | ||
|
||
module GH | ||
# Public: A Wrapper class that deals with normalizing Github responses. | ||
class Normalizer < Wrapper | ||
# Public: Fetches resource from Github and normalizes the response. | ||
# | ||
# Returns normalized Response. | ||
def [](key) | ||
normalize super | ||
end | ||
|
||
private | ||
|
||
def set_link(hash, type, href) | ||
links = hash["_links"] ||= {} | ||
links[type] = {"href" => href} | ||
end | ||
|
||
def normalize_response(response) | ||
response = response.dup | ||
response.data = normalize response.data | ||
response | ||
end | ||
|
||
def normalize_hash(hash) | ||
corrected = {} | ||
|
||
hash.each_pair do |key, value| | ||
key = normalize_key(key, value) | ||
next if normalize_url(corrected, key, value) | ||
next if normalize_time(corrected, key, value) | ||
corrected[key] = normalize(value) | ||
end | ||
|
||
normalize_user(corrected) | ||
corrected | ||
end | ||
|
||
def normalize_time(hash, key, value) | ||
hash['date'] = Time.at(value).xmlschema if key == 'timestamp' | ||
end | ||
|
||
def normalize_user(hash) | ||
hash['owner'] ||= hash.delete('user') if hash['created_at'] and hash['user'] | ||
hash['author'] ||= hash.delete('user') if hash['committed_at'] and hash['user'] | ||
|
||
hash['committer'] ||= hash['author'] if hash['author'] | ||
hash['author'] ||= hash['committer'] if hash['committer'] | ||
end | ||
|
||
def normalize_url(hash, key, value) | ||
case key | ||
when "blog" | ||
set_link(hash. key, value) | ||
when "url" | ||
type = Addressable::URI.parse(value).host == api_host.host ? "self" : "html" | ||
set_link(hash, type, value) | ||
when /^(.+)_url$/ | ||
set_link(hash, $1, value) | ||
end | ||
end | ||
|
||
def normalize_key(key, value = nil) | ||
case key | ||
when 'gravatar_url' then 'avatar_url' | ||
when 'org' then 'organization' | ||
when 'orgs' then 'organizations' | ||
when 'username' then 'login' | ||
when 'repo' then 'repository' | ||
when 'repos' then normalize_key('repositories', value) | ||
when /^repos?_(.*)$/ then "repository_#{$1}" | ||
when /^(.*)_repo$/ then "#{$1}_repository" | ||
when /^(.*)_repos$/ then "#{$1}_repositories" | ||
when 'commit', 'commit_id' then value =~ /^\w{40}$/ ? 'sha' : key | ||
when 'comments' then Number === value ? 'comment_count' : key | ||
when 'forks' then Number === value ? 'fork_count' : key | ||
when 'repositories' then Number === value ? 'repository_count' : key | ||
when /^(.*)s_count$/ then "#{$1}_count" | ||
else key | ||
end | ||
end | ||
|
||
def normalize_array(array) | ||
array.map { |e| normalize(e) } | ||
end | ||
|
||
def normalize(object) | ||
case object | ||
when Hash then normalize_hash(object) | ||
when Array then normalize_array(object) | ||
when Response then normalize_response(object) | ||
else object | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
require 'gh' | ||
require 'gh/faraday' | ||
|
||
module GH | ||
# Public: This class deals with HTTP requests to Github. It is the base Wrapper you always want to use. | ||
# Note that it is usually used implicitely by other wrapper classes if not specified. | ||
class Remote < Wrapper | ||
attr_reader :api_host, :connection, :headers | ||
|
||
# Public: Generates a new Rempte instance. | ||
# | ||
# api_host - HTTP host to send requests to, has to include schema (https or http) | ||
# options - Hash with configuration options: | ||
# :token - OAuth token to use (optional). | ||
# :username - Github user used for login (optional). | ||
# :password - Github password used for login (optional). | ||
# :origin - Value of the origin request header (optional). | ||
# :headers - HTTP headers to be send on every request (optional). | ||
# :adapter - HTTP library to use for making requests (optional, default: :net_http) | ||
# | ||
# It is highly recommended to set origin, but not to set headers. | ||
# If you set the username, you should also set the password. | ||
def initialize(api_host = 'https://api.github.com', options = {}) | ||
api_host, options = normalize_options(api_host, options) | ||
token, username, password = options.values_at :token, :username, :password | ||
|
||
api_host = api_host.api_host if api_host.respond_to? :api_host | ||
@api_host = Addressable::URI.parse(api_host) | ||
@headers = options[:headers].try(:dup) || { | ||
"Origin" => options[:origin] || "http://example.org", | ||
"Accept" => "application/vnd.github.v3.raw+json," \ | ||
"application/vnd.github.beta.raw+json;q=0.5," \ | ||
"application/json;q=0.1", | ||
"Accept-Charset" => "utf-8" | ||
} | ||
|
||
@connection = Faraday.new(:url => api_host) do |builder| | ||
builder.request(:token_auth, token) if token | ||
builder.request(:basic_auth, username, password) if username and password | ||
builder.request(:retry) | ||
builder.response(:raise_error) | ||
builder.adapter(options[:adapter] || :net_http) | ||
end | ||
end | ||
|
||
# Public: Retrieves resources from Github. | ||
# | ||
# Examples | ||
# | ||
# Github::Remote.new['users/rkh'] # => { ... } | ||
# | ||
# Raises Faraday::Error::ResourceNotFound if the resource returns status 404. | ||
# Raises Faraday::Error::ClientError if the resource returns a status between 400 and 599. | ||
# Returns the Response. | ||
def [](key) | ||
response = connection.get(path_for(key), headers) | ||
Response.new(response.headers, response.body) | ||
end | ||
end | ||
end |
Oops, something went wrong.