├── .rspec ├── .gitignore ├── Gemfile ├── lib ├── github-api-client │ ├── version.rb │ ├── fetchers.rb │ ├── strategies │ │ ├── local.rb │ │ ├── remote.rb │ │ └── ask.rb │ ├── strategy.rb │ ├── helpers.rb │ ├── resources │ │ ├── user.rb │ │ └── repo.rb │ ├── fetchers │ │ ├── repo.rb │ │ └── user.rb │ ├── browser.rb │ ├── config.rb │ ├── resource.rb │ └── base.rb ├── core_ext │ └── habtm.rb └── github-api-client.rb ├── spec ├── test_spec.rb └── other_spec.rb ├── TODO ├── bin ├── api-browser.rb └── github-api-client ├── db └── migrate │ └── 001_migrate_everything.rb ├── features ├── support │ └── env.rb ├── fetching.feature ├── step_definitions │ └── fetching_steps.rb └── user_api.feature ├── Rakefile ├── NEWS ├── LICENSE.txt ├── README.md ├── Gemfile.lock └── github-api-client.gemspec /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /doc/ 3 | .yardoc 4 | *.swp 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/github-api-client/version.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | module Version 3 | Major = 0 4 | Minor = 4 5 | Patch = 0 6 | Build = 'pre' 7 | 8 | String = [Major, Minor, Patch, Build].join('.') 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/github-api-client/fetchers.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/hash' 2 | 3 | module GitHub 4 | module Fetchers 5 | class << self 6 | def parse(data) 7 | JSON.parse(data, symbolize_names: true) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/core_ext/habtm.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do 2 | # Short alias for if exists then create 3 | # @param [ActiveRecord::Base] object An object to check if exists and create 4 | def find_or_create(object) 5 | self.concat(object) unless include?(object) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/test_spec.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | MiniTest::Unit.autorun 3 | require 'rspec/expectations' 4 | require 'rspec/matchers' 5 | require 'mocha' 6 | 7 | describe "Testing framework" do 8 | it "executes" do 9 | user = Struct.new("User", :name).new('kuba') 10 | user.stubs(:age).returns(3) 11 | 3.should eq(user.age) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/github-api-client/strategies/local.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | # always refresh 3 | module Strategies 4 | module Local 5 | class << self 6 | def should_refresh?(model) 7 | return false 8 | end 9 | 10 | def update_strategy(model); end # no need for updating on this strategy 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/github-api-client/strategies/remote.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | # always refresh 3 | module Strategies 4 | module Remote 5 | class << self 6 | def should_refresh?(model) 7 | return true 8 | end 9 | 10 | def update_strategy(model); end # no need for updating on this strategy 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/other_spec.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | MiniTest::Unit.autorun 3 | require 'rspec/expectations' 4 | require 'rspec/matchers' 5 | require 'mocha' 6 | 7 | describe "Testing framework other" do 8 | it "executes" do 9 | user = stub(:money => 50) 10 | order = stub(:total_amount => 49.99) 11 | 12 | user.money.should satisfy { |money| money > order.total_amount } 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/github-api-client/strategy.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | class CachingStrategyNotImplemented < StandardError; end 3 | # Template 4 | module CachingStrategy 5 | class << self 6 | def should_refresh?(model) 7 | throw CachingStrategyNotImplemented 8 | end 9 | 10 | def update_strategy(model) 11 | throw CachingStrategyNotImplemented 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/github-api-client/strategies/ask.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | module Strategies 3 | module Ask 4 | class << self 5 | def should_refresh?(model) 6 | puts "Should I refresh #{Helpers.const_name(model)}? [y/n]" 7 | return gets.chomp == 'y' 8 | end 9 | 10 | def update_strategy(model); end # no need for updating on this strategy 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * checking how many requests are left (from http headers [caching preffered from last request]) 2 | * helper methods for fetching data 3 | | writing GitHub::Fetchers::Repository.get(resource_user.login + '/' + resource_user.repos.select{|repo| repo.name == 'github-api-client'}) # is not fun! 4 | | rethink design fetchers? 5 | * helpers for choosing strategy (maybe providing short descriptions?) 6 | * use minitest for testing with rspec matchers 7 | -------------------------------------------------------------------------------- /lib/github-api-client/helpers.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | module Helpers 3 | class << self 4 | def const_at(sym, scope) 5 | throw ArgumentError, "first parameter must be a symbol" unless sym.is_a? Symbol 6 | throw ArgumentError, "scope must be a module" unless scope.is_a? Module 7 | return scope.const_get(sym) 8 | end 9 | 10 | def const_name(object) 11 | return object.class.name.split('::').last.to_sym 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/github-api-client/resources/user.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | module Resources 3 | class User 4 | @@attributes = {login: :string, name: :string, location: :string, bio: :string, email: :string, hireable: :boolean, blog: :string} 5 | @@pushables = [:name, :location, :bio, :email, :hireable, :blog, :company] 6 | @@associations = {repositories: [nil, -> { has_many :repositories, class_name: 'GitHub::Storage::Repository', foreign_key: :owner_id}]} 7 | include Resource 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /bin/api-browser.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('../lib', File.dirname(__FILE__)) 4 | 5 | require 'github-api-client/resource' 6 | 7 | class User 8 | @@attributes = {login: String, name: String, has_repos: true, location: String} 9 | @@pushables = [:name, :location] 10 | @@associations = {repositories: [nil, -> { has_many :repositories, :class_name => Repo}]} 11 | include Resource 12 | end 13 | 14 | u = User.new 15 | u.name = 'Kuba' 16 | u.attributes[:has_repos] = true 17 | u.save 18 | p u.repositories 19 | -------------------------------------------------------------------------------- /db/migrate/001_migrate_everything.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/string' 2 | 3 | class MigrateEverything < ActiveRecord::Migration 4 | def change 5 | GitHub::Resources.constants.each do |resource| 6 | klass = GitHub::Resources.const_get(resource) 7 | create_table resource.to_s.pluralize.downcase do |table| 8 | klass.class_variable_get(:@@attributes).each_pair do |key, value| 9 | table.send(value, key) 10 | end 11 | 12 | klass.class_variable_get(:@@associations).each_pair do |key, value| 13 | table.references value.first unless value.first.nil? 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | begin 3 | Bundler.setup(:default, :development) 4 | rescue Bundler::BundlerError => e 5 | $stderr.puts e.message 6 | $stderr.puts "Run `bundle install` to install missing gems" 7 | exit e.status_code 8 | end 9 | 10 | $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib') 11 | require 'github-api-client' 12 | 13 | require 'rspec/expectations' 14 | require 'stringio' 15 | require "mocha" 16 | 17 | # Mocha integration into Cucumber 18 | # source: https://gist.github.com/80554 19 | World(Mocha::API) 20 | 21 | Before do 22 | mocha_setup 23 | end 24 | 25 | After do 26 | begin 27 | mocha_verify 28 | ensure 29 | mocha_teardown 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /features/fetching.feature: -------------------------------------------------------------------------------- 1 | Feature: Fetching Objects 2 | In order to fetch objects from GitHub 3 | I just need to request a function from a model 4 | 5 | Scenario: Fetching user information 6 | Given I fetch user "schacon" 7 | Then my local database should contain that record 8 | And that record's "name" should be "Scott Chacon" 9 | 10 | Scenario: Fetching repo information 11 | Given I fetch repo "mojombo/jekyll" 12 | Then my local database should contain that record 13 | And that record's "login" of the "owner" should be "mojombo" 14 | 15 | Scenario: Fetching organization 16 | Given I fetch organization "github" 17 | Then my local database should contain that record 18 | And that record's "email" should be "support@github.com" 19 | -------------------------------------------------------------------------------- /lib/github-api-client/resources/repo.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | module Resources 3 | class Repository 4 | @@attributes = {name: :string, description: :string, homepage: :string, private: :boolean, fork: :boolean, language: :string, master_branch: :string, size: :integer, pushed_at: :datetime, created_at: :datetime, has_issues: :boolean, has_wiki: :boolean, has_downloads: :boolean, permalink: :string} 5 | @@pushables = [:name, :description, :homepage, :private, :has_issues, :has_wiki, :has_downloads] #team_id 6 | @@associations = {parent: [:parent, -> { belongs_to :parent, :class_name => 'GitHub::Storage::Repository'}], 7 | contributors: [nil, -> {}], # habtm 8 | owner: [:owner, -> { belongs_to :owner, class_name: 'GitHub::Storage::User'}] 9 | } 10 | include Resource 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | include Rake::DSL 6 | begin 7 | Bundler.setup(:default, :development) 8 | rescue Bundler::BundlerError => e 9 | $stderr.puts e.message 10 | $stderr.puts "Run `bundle install` to install missing gems" 11 | exit e.status_code 12 | end 13 | require 'rake' 14 | include Rake::DSL # supressess jeweler warnings 15 | 16 | desc 'run irb session against this library' 17 | task :irb do 18 | system 'irb -I./lib -r github-api-client' 19 | end 20 | 21 | desc 'run test suite' 22 | task :tests do 23 | Dir[File.dirname(__FILE__) + '/spec/*_spec.rb'].each do |file| 24 | require file 25 | end 26 | end 27 | 28 | require 'cucumber/rake/task' 29 | Cucumber::Rake::Task.new(:features) 30 | 31 | task :default => :spec 32 | 33 | require 'yard' 34 | YARD::Rake::YardocTask.new 35 | -------------------------------------------------------------------------------- /features/step_definitions/fetching_steps.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | %w(Repo User Organization).each do |attr| 3 | GitHub.const_get(attr).delete_all 4 | end 5 | end 6 | 7 | Given /^I fetch user "(.*)"$/ do |login| 8 | @record = GitHub::User.get(login) 9 | end 10 | 11 | Given /^I fetch repo "(.*)"$/ do |permalink| 12 | @record = GitHub::Repo.get(permalink) 13 | end 14 | 15 | Given /^I fetch organization "(.*)"$/ do |login| 16 | @record = GitHub::Organization.get(login) 17 | end 18 | 19 | Then /^my local database should contain that record$/ do 20 | @record.class.find(@record.id).should == @record 21 | end 22 | 23 | Then /^that record's "([^"]*)" should be "([^"]*)"$/ do |sig, prop| 24 | @record.send(sig.to_sym).should == prop 25 | end 26 | 27 | Then /^that record's "([^"]*)" of the "([^"]*)" should be "([^"]*)"$/ do |sig, type, prop| 28 | @record.send(type.to_sym).send(sig.to_sym).should == prop 29 | end 30 | 31 | Given /^I set verbose option to "(.*)"$/ do |bool| 32 | GitHub::Config::Options[:verbose] = bool 33 | end 34 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | github-api-client NEWS -- history of user-visible changes 2 | updated at 2011/01/30 3 | 4 | Version 0.4.0.pre 5 | * This version serves the purpose of showing new features and new design that github-api-client implemented recently 6 | * New design 7 | focuses on splitting the logic into: 8 | - Models (backend) - generally hidden from end-user 9 | - Fetchers (frontend) - retrieve data from the site and store them locally using strategies 10 | * Strategy - defines when to refresh data 11 | - Resources (frontend) - light objects with @attributes hash and defined accessors for fields (also for editable fields) 12 | 13 | * Resource description 14 | classes like Repository or User are now inheriting from Resource class which generates accessor methods for them and more 15 | 16 | * Fully dynamic models and migrations generation 17 | taking advantage from using Resource classes to describe API resources, migrations now create tables for every resource 18 | models also include associations defined in Resource classes 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jakub Okoński 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /bin/github-api-client: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'optparse' 5 | require 'github-api-client' 6 | 7 | # These are the default option values. By setting them 8 | # before we parse the arguments, we make sure we don't have 9 | # any missing argument wonkyness 10 | GitHub::Config::Options[:verbose] = false 11 | GitHub::Config::Options[:server] = 'github.com' 12 | GitHub::Config::Options[:reset_db] = false 13 | 14 | OptionParser.new { |opts| 15 | 16 | opts.on( '-h', '--help', 'Display this screen' ) do 17 | puts opts 18 | end 19 | opts.on( '-v', '--verbose', 'Enable Verbose Output') do 20 | GitHub::Config::Options[:verbose] = true 21 | end 22 | opts.on( '-s', '--server SERVER', 'Change the GitHub server') do |host| 23 | GitHub::Config::Options[:server] = host 24 | end 25 | opts.on( '-r', '--reset-db', 'Reset the database') do 26 | GitHub::Config::Options[:reset_db] = true 27 | end 28 | 29 | }.parse! 30 | 31 | GitHub::Config::Options[:reset_db] ? GitHub::Config.reset : GitHub::Config.setup 32 | 33 | puts GitHub::Organization.get('rails').fetch(:repositories).repositories.watchers.map { |x| x.attributes } 34 | -------------------------------------------------------------------------------- /lib/github-api-client/fetchers/repo.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | module Fetchers 3 | module Repository 4 | class << self 5 | def get(permalink) 6 | attributes = {} 7 | name = permalink.split('/').last 8 | owner = Models::User.find_or_create_by_login(permalink.split('/').first) 9 | model = owner.repositories.find_by_name(name) 10 | model ||= nil 11 | should_refresh = model ? Config::Options[:strategy].should_refresh?(model) : true 12 | if should_refresh 13 | Browser.start do |http| 14 | request = Net::HTTP::Get.new "/repos/#{permalink}" 15 | attributes = Fetchers.parse(http.request(request).body) 16 | model = Models::Repository.find_or_create_by_permalink(permalink) 17 | model.owner = owner 18 | model.update_attributes(Resources::Repository.valid_attributes(attributes)) 19 | end 20 | end 21 | repo = Resources::Repository.new.tap do |repo| 22 | repo.attributes = Resources::Repository.valid_attributes(model.attributes.symbolize_keys) 23 | end 24 | end 25 | 26 | def association_owner(repo) 27 | User.get(repo.model.owner.login) # requires #get function to associate owner *always* 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-api-client 2 | 3 | GitHub API Client that supports caching content client-side, allowing every later requests to be **instant**. 4 | 5 | # Configuration 6 | You have three ways of defining your user to have authenticated access to your API: 7 | 8 | 1. Put a file in: ~/.github/secrets.yml or \Users\username\.github\secrets.yml 9 | # secrets.yml 10 | user: 11 | login: your_login 12 | token: your_token 13 | 2. Put GITHUB_USER and GITHUB_TOKEN in your environment, so github-api-client can read it. 14 | 3. Configure your global git profile as defined here http://help.github.com/git-email-settings 15 | 16 | ## Contributing to github-api-client 17 | 18 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 19 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 20 | * Fork the project 21 | * Start a feature/bugfix branch 22 | * Commit and push until you are happy with your contribution 23 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 24 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 25 | 26 | ## Copyright 27 | 28 | Copyright (c) 2012 Jakub Okoński. See LICENSE.txt for 29 | further details. 30 | 31 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | github-api-client (0.4.0.pre) 5 | OptionParser (>= 0.5.1) 6 | activerecord (>= 3.1.0) 7 | activesupport (>= 3.1.0) 8 | rainbow (>= 1.1.3) 9 | rake (= 0.8.7) 10 | sqlite3 (>= 1.3.5) 11 | 12 | GEM 13 | remote: http://rubygems.org/ 14 | specs: 15 | OptionParser (0.5.1) 16 | activemodel (3.2.1) 17 | activesupport (= 3.2.1) 18 | builder (~> 3.0.0) 19 | activerecord (3.2.1) 20 | activemodel (= 3.2.1) 21 | activesupport (= 3.2.1) 22 | arel (~> 3.0.0) 23 | tzinfo (~> 0.3.29) 24 | activesupport (3.2.1) 25 | i18n (~> 0.6) 26 | multi_json (~> 1.0) 27 | arel (3.0.0) 28 | builder (3.0.0) 29 | cucumber (1.1.4) 30 | builder (>= 2.1.2) 31 | diff-lcs (>= 1.1.2) 32 | gherkin (~> 2.7.1) 33 | json (>= 1.4.6) 34 | term-ansicolor (>= 1.0.6) 35 | diff-lcs (1.1.3) 36 | gherkin (2.7.6) 37 | json (>= 1.4.6) 38 | i18n (0.6.0) 39 | json (1.6.5) 40 | metaclass (0.0.1) 41 | minitest (2.11.0) 42 | mocha (0.10.3) 43 | metaclass (~> 0.0.1) 44 | multi_json (1.0.4) 45 | rainbow (1.1.3) 46 | rake (0.8.7) 47 | rspec-expectations (2.8.0) 48 | diff-lcs (~> 1.1.2) 49 | sqlite3 (1.3.5) 50 | term-ansicolor (1.0.7) 51 | tzinfo (0.3.31) 52 | yard (0.7.4) 53 | 54 | PLATFORMS 55 | ruby 56 | 57 | DEPENDENCIES 58 | cucumber (>= 1.1.4) 59 | github-api-client! 60 | minitest (>= 2.11.0) 61 | mocha (>= 0.10.0) 62 | rspec-expectations (>= 2.7.0) 63 | yard (>= 0.6.0) 64 | -------------------------------------------------------------------------------- /github-api-client.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | $:.unshift File.expand_path("../lib", __FILE__) 4 | require "github-api-client/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "github-api-client" 8 | s.version = GitHub::Version::String 9 | 10 | s.authors = [%{Jakub Okoński}] 11 | s.date = "2012-01-30" 12 | s.description = "Caches retrieved information to your user profile and reuses it when you query again." 13 | s.email = "kuba@okonski.org" 14 | s.executables = ["api-browser.rb github-api-client"] 15 | s.extra_rdoc_files = [ 16 | "LICENSE.txt", 17 | "README.md", 18 | "TODO", 19 | "NEWS" 20 | ] 21 | s.files = `git ls-files`.split("\n") 22 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 23 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 24 | 25 | s.homepage = %{http://github.com/farnoy/github-api-client} 26 | s.licenses = ["MIT"] 27 | s.require_paths = ["lib"] 28 | s.summary = %{Library for easy GitHub API browsing} 29 | 30 | s.add_runtime_dependency("rainbow", [">= 1.1.3"]) 31 | s.add_runtime_dependency("rake", ["= 0.8.7"]) 32 | s.add_runtime_dependency("activerecord", [">= 3.1.0"]) 33 | s.add_runtime_dependency("activesupport", [">= 3.1.0"]) 34 | s.add_runtime_dependency("sqlite3", [">= 1.3.5"]) 35 | s.add_runtime_dependency("OptionParser", [">= 0.5.1"]) 36 | 37 | s.add_development_dependency("minitest", [">= 2.11.0"]) 38 | s.add_development_dependency("rspec-expectations", [">= 2.7.0"]) 39 | s.add_development_dependency("mocha", [">= 0.10.0"]) 40 | s.add_development_dependency("yard", [">= 0.6.0"]) 41 | s.add_development_dependency("cucumber", [">= 1.1.4"]) 42 | end 43 | -------------------------------------------------------------------------------- /lib/github-api-client.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | ROOT = File.expand_path('../', File.dirname(__FILE__)) 4 | 5 | $:.unshift File.dirname(__FILE__) 6 | 7 | require 'net/http' 8 | require 'uri' 9 | require 'json' 10 | require 'singleton' 11 | require 'active_record' 12 | require 'core_ext/habtm' 13 | require 'rainbow' 14 | require 'github-api-client/version' 15 | require 'github-api-client/config' 16 | require 'github-api-client/strategy' 17 | 18 | require 'github-api-client/base' 19 | #require 'github-api-client/user' 20 | #require 'github-api-client/repo' 21 | #require 'github-api-client/organization' 22 | require 'github-api-client/browser' 23 | require 'github-api-client/helpers' 24 | 25 | # Resources 26 | require 'github-api-client/resource' 27 | Dir[File.expand_path("github-api-client/resources/*.rb", File.dirname(__FILE__))].each do |lib| 28 | require lib 29 | end 30 | 31 | # This hard-coded if's will be soon replaced by Option Parser 32 | GitHub::Config::Options[:verbose] = true if ARGV.include? '--verbose' 33 | if ARGV.include? '--reset-db' 34 | GitHub::Config.reset 35 | else 36 | GitHub::Config.setup 37 | end 38 | 39 | # Fetchers 40 | require 'github-api-client/fetchers' 41 | Dir[File.expand_path("github-api-client/fetchers/*.rb", File.dirname(__FILE__))].each do |lib| 42 | require lib 43 | end 44 | 45 | #unless $user = GitHub::User.where(GitHub::Config::Secrets).first 46 | # $user = GitHub::User.create(GitHub::Config::Secrets) 47 | #end if GitHub::Config::Secrets 48 | 49 | 50 | # Strategies 51 | require 'github-api-client/strategies/ask' 52 | require 'github-api-client/strategies/remote' 53 | require 'github-api-client/strategies/local' 54 | GitHub::Config::Options[:strategy] = GitHub::Strategies::Ask # GitHub::CachingStrategy 55 | 56 | 57 | # General placeholder for all of the GitHub API sweets 58 | module GitHub 59 | end 60 | -------------------------------------------------------------------------------- /lib/github-api-client/browser.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | # Handles low-level HTTP requests 3 | class Browser 4 | include Singleton 5 | 6 | # Returnes root uri for GitHub API 7 | # @param [Object] *options only for backwards compatibility! 8 | # @return [String] Base GitHub API url 9 | def self.base_uri(*options) 10 | gh_uri = GitHub::Config::Options[:server]||'api.github.com' 11 | "http://#{gh_uri}/" 12 | end 13 | 14 | # Sets up a net/http connection 15 | # @return [http transaction] transaction to github 16 | def self.start(&block) 17 | Net::HTTP.start(URI.parse(self.base_uri).host, :use_ssl => true, &block) 18 | end 19 | 20 | # Runs HTTP GET request at given uri 21 | # @param [String] uri URI to be joined with base_uri and requested 22 | # @return [String] request result 23 | def self.get(uri, version = 'v2') 24 | uri = URI.parse(self.base_uri(version) + uri.gsub(" ","+")) 25 | puts "Requesting #{uri}" if GitHub::Config::Options[:verbose] 26 | Net::HTTP.get uri 27 | end 28 | 29 | # Runs HTTP POST requests with options such as GitHub::User.auth_info 30 | # @param [String] uri URI to be joined with base_uri and requested 31 | # @return [String] request result 32 | def self.post(uri, options = {}, version = 'v2') 33 | uri = URI.parse(self.base_uri(version) + uri.gsub(" ","+")) 34 | puts "Requesting #{uri} with options: #{options}" if GitHub::Config::Options[:verbose] 35 | Net::HTTP.post_form uri, options 36 | end 37 | 38 | # Runs HTTP PATCH request at a given uri 39 | # @param [String] uri URI to be joined with base_uri and requested 40 | # @return [String] request result 41 | def self.patch(uri, options = {}, version = 'v2') 42 | uri = uri.gsub(" ","+") 43 | puts "Requesting #{URI.parse(self.base_uri(version) + uri)} with options: #{options}" if GitHub::Config::Options[:verbose] 44 | Net::HTTP.patch URI.parse(self.base_uri + uri), options 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/github-api-client/fetchers/user.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | module Fetchers 3 | module User 4 | def self.get(login) 5 | attributes = {} 6 | model = Storage::User.find_by_login(login) 7 | should_refresh = model ? Config::Options[:strategy].should_refresh?(model) : true 8 | if should_refresh 9 | Browser.start do |http| 10 | request = Net::HTTP::Get.new "/users/#{login}" 11 | attributes = Fetchers.parse(http.request(request).body) 12 | model = Storage::User.find_or_create_by_login(login) 13 | model.update_attributes(Resources::User.valid_attributes(attributes)) 14 | end 15 | end 16 | Resources::User.new.tap do |user| 17 | user.attributes = model.attributes.symbolize_keys! 18 | end 19 | end 20 | 21 | def self.association_repositories(user) 22 | attributes = {} 23 | models = (um = Storage::User.find_by_name(user.name)) ? um.repositories : [] 24 | should_refresh = models.empty? ? true : Config::Options[:strategy].should_refresh?(models) 25 | if should_refresh 26 | models = [] # ensure empty when refreshing 27 | Browser.start do |http| 28 | request = Net::HTTP::Get.new "/users/#{user.login}/repos" 29 | attributes = Fetchers.parse(http.request(request).body) 30 | end 31 | 32 | ActiveRecord::Base.transaction do 33 | attributes.each do |repo| 34 | permalink = repo[:owner][:login] + '/' + repo[:name] 35 | models << Storage::Repository.find_or_create_by_permalink(permalink) 36 | models.last.update_attributes(Resources::Repository.valid_attributes(repo)) 37 | end 38 | end 39 | end 40 | collection = [] 41 | models.each do |model| 42 | collection << Resources::Repository.new.tap do |repo| 43 | repo.attributes = Resources::Repository.valid_attributes(model.attributes.symbolize_keys!) 44 | end 45 | end 46 | return collection 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/github-api-client/config.rb: -------------------------------------------------------------------------------- 1 | module GitHub 2 | # Keeps all the configuration stuff 3 | module Config 4 | # Constant with defined all the paths used in the application 5 | Path = { 6 | :dir => ENV['HOME'] + "/.github", 7 | :dbfile => ENV['HOME'] + "/.github/github.db", 8 | :migrations => ROOT + "/db/migrate", 9 | :secrets => ENV['HOME'] + "/.github" + "/secrets.yml" 10 | } 11 | 12 | # Secrets array, uses env vars if defined 13 | Secrets = case 14 | when ENV['GITHUB_USER'] && ENV['GITHUB_TOKEN'] 15 | { 16 | "login" => ENV['GITHUB_USER'], 17 | "token" => ENV['GITHUB_TOKEN'] 18 | } 19 | when `git config --global github.user` && !`git config --global github.token` 20 | { 21 | "login" => `git config --global github.user`.strip, 22 | "token" => `git config --global github.token`.strip 23 | } if `git config --global github.user` && !`git config --global github.token` 24 | else 25 | begin 26 | # If not env vars, then ~/.github/secrets.yml 27 | YAML::load_file(GitHub::Config::Path[:secrets])['user'] 28 | rescue Errno::ENOENT 29 | # Eye candy with rainbow 30 | puts <<-report 31 | You have three ways of defining your user to have authenticated access to your API: 32 | #{"1.".color(:cyan)} Put a file in: #{GitHub::Config::Path[:secrets].color(:blue).bright} 33 | Define in yaml: 34 | #{"user".color(:yellow).bright}: 35 | #{"login".color(:green).bright}: #{"your_login".color(:magenta)} 36 | #{"token".color(:blue).bright}: #{"your_token".color(:magenta)} 37 | #{"2.".color(:cyan)} Put #{"GITHUB_USER".color(:green).bright} and #{"GITHUB_TOKEN".color(:blue).bright} in your environment, so github-api-client can read it. 38 | #{"3.".color(:cyan)} Configure your global git profile as defined here #{"http://help.github.com/git-email-settings/".color(:blue).bright} 39 | 40 | report 41 | end 42 | end 43 | 44 | Options = { 45 | :verbose => false 46 | } 47 | 48 | # Sets up the database and migrates it 49 | # @return [nil] 50 | def self.setup 51 | Dir.mkdir GitHub::Config::Path[:dir] rescue nil 52 | ActiveRecord::Base.establish_connection( 53 | :adapter => 'sqlite3', 54 | :database => GitHub::Config::Path[:dbfile] 55 | ) 56 | ActiveRecord::Migrator.migrate( 57 | GitHub::Config::Path[:migrations], 58 | nil 59 | ) if not File.exists? GitHub::Config::Path[:dbfile] 60 | end 61 | 62 | def self.reset 63 | File.delete Path[:dbfile] 64 | setup 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/github-api-client/resource.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'active_model/dirty' 3 | require 'active_support/core_ext/string' 4 | 5 | module Resource 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | attr_accessor :attributes 10 | include ActiveModel::Dirty 11 | 12 | define_attribute_methods = class_variable_get(:@@pushables) 13 | 14 | define_method :initialize do 15 | instance_variable_set(:@attributes, {}) 16 | instance_variable_set(:@changed_attributes, {}) 17 | end 18 | 19 | class_variable_get(:@@attributes).each_pair do |key, value| 20 | method_name = (if value == :boolean then "#{key}?"; else key; end) 21 | define_method method_name do 22 | return self.instance_variable_get(:@attributes)[key] 23 | end 24 | define_method "#{key}=" do |o| 25 | send(:attribute_will_change!, key) unless o == self.instance_variable_get(:@attributes)[key] 26 | return instance_variable_get(:@attributes)[key] = o 27 | end if class_variable_get(:@@pushables).include? key 28 | end 29 | 30 | define_singleton_method :valid_attributes do |options| 31 | options.select do |element| 32 | class_variable_get(:@@attributes).include?(element) or element == :id 33 | end 34 | end 35 | 36 | class_variable_get(:@@associations).each_pair do |key, value| 37 | define_method key do 38 | return GitHub::Fetchers.const_get(GitHub::Helpers.const_name(self)).send(:"association_#{key}", self) 39 | end 40 | end 41 | 42 | define_method :save do 43 | @changed_attributes.clear 44 | end 45 | 46 | define_method :model do 47 | GitHub::Helpers.const_at(GitHub::Helpers.const_name(self), GitHub::Storage).find(instance_variable_get(:@attributes)[:id]) 48 | end 49 | 50 | define_method :inspect do 51 | s = "# :nil, :public_gist_count => :nil, :created => :nil, :permission => :nil, :followers_count => :nil, :following_count => :nil} 48 | when :user_search then {:name => :login, :username => :login, :fullname => :name, :followers => :nil, :repos => :nil, :created => :nil, :permission => :nil} 49 | when :repo_get then {:fork => :b_fork, :watchers => nil, :owner => :owner_login, :forks => nil, :followers_count => nil, :forks_count => nil, :master_branch => nil} 50 | when :org_get then {:public_gist_count => nil, :public_repo_count => nil, :following_count => :nil, :followers_count => :nil} 51 | when :org_repo_index then {:owner => nil, :open_issues => nil, :has_issues => nil, :watchers => nil, :forks => nil, :fork => :b_fork, :gravatar_id => nil, :organization => :organization_login, :master_branch => nil} 52 | when :org_repo_get then {:owner => nil, :open_issues => nil, :has_issues => nil, :watchers => nil, :forks => nil, :fork => :b_fork, :gravatar_id => nil, :organization => :organization_login} 53 | else raise "Unknown resource #{resource.inspect} with attributes #{attributes.inspect}" 54 | end 55 | # Provides abstraction layer between YAML :keys and 'keys' returned by Hub 56 | symbolized_resources = [:repo_get, :org_repo_index, :org_repo_get] 57 | hash.each do |k, v| 58 | unless v == :nil || v == nil 59 | if v.class != Symbol 60 | attributes[k.to_s] = v 61 | else 62 | if symbolized_resources.include? resource 63 | attributes[v.to_s] = attributes[k.to_sym] 64 | else 65 | attributes[v.to_s] = attributes[k.to_s] 66 | end 67 | end 68 | end 69 | if symbolized_resources.include? resource 70 | attributes.delete k.to_sym 71 | else 72 | attributes.delete k.to_s 73 | end 74 | end 75 | attributes 76 | end 77 | 78 | # ActiveRecord fix that returns attributes 79 | # @return [Hash] Attributes of the object 80 | def to_ary 81 | return self.attributes 82 | end 83 | end 84 | 85 | # Singleton class, that is used globally 86 | class Helper 87 | include Singleton 88 | 89 | # Recognizing objects retrieved from GitHub, creating new and assigning parameters 90 | # from YAML 91 | # === Objects 92 | # * GitHub::User - recognition by key 'user' 93 | # More to be added soon 94 | # @deprecated Nothing uses it, but may come handy later 95 | # @param [String] yaml a YAML content to be parsed 96 | # @return [GitHub::User, Array] 97 | def self.build_from_yaml(yaml) 98 | yaml = YAML::load yaml 99 | object = case 100 | when yaml.has_key?('user') then [GitHub::User, 'user'] 101 | when yaml.has_key?('users') then [[GitHub::User], 'users'] 102 | end 103 | if object.first.class == Array 104 | objects = [] 105 | yaml[object[1]].each do |single_yaml| 106 | o = object.first.first.new 107 | o.build single_yaml 108 | objects << o 109 | end 110 | objects 111 | else 112 | object[0] = object.first.new 113 | object.first.build yaml[object[1]] 114 | object.first 115 | end 116 | end 117 | end 118 | end 119 | --------------------------------------------------------------------------------