fcoury-octopi 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -2,7 +2,53 @@
2
2
 
3
3
  Octopi is a Ruby interface to GitHub API v2 (http://develop.github.com). It's under early but active development and already works.
4
4
 
5
- == Example
5
+ == Authenticated Usage
6
+
7
+ The following examples requires a valid user authenticated with GitHub using login and API token. This information can be found on our profile (the link is inside the "badge" that is displayed on the upper right corner when you're logged in).
8
+
9
+ Once you found your login and token you can run authenticated commands using:
10
+
11
+ authenticated_with "mylogin", "mytoken" do |g|
12
+ repo = g.repository("api-labrat")
13
+ issue = repo.open_issue :title => "Sample issue",
14
+ :body => "This issue was opened using GitHub API and Octopi"
15
+ puts issue.number
16
+ end
17
+
18
+ You can also create a YAML file with your information, with the following format:
19
+
20
+ #
21
+ # Octopi GitHub API configuration file
22
+ #
23
+
24
+ # GitHub user login and token
25
+ login: github-username
26
+ token: github-token
27
+
28
+ # Trace level
29
+ # Possible values:
30
+ # false - no tracing, same as if the param is ommited
31
+ # true - will output each POST or GET operation to the stdout
32
+ # curl - same as true, but in addition will output the curl equivalent of each command (for debugging)
33
+ trace: curl
34
+
35
+ And change the way you connect to:
36
+
37
+ authenticated_with :config => "github.yml" do |g|
38
+ (...)
39
+ end
40
+
41
+ This way you can benefit from better debugging when something goes wrong. If you choose curl tracing, the curl command equivalent to each command sent to GitHub will be output to the stdout, like this example:
42
+
43
+ => Trace on: curl
44
+ POST: /issues/open/webbynode/api-labrat params: body=This issue was opened using GitHub API and Octopi, title=Sample issue
45
+ ===== curl version
46
+ curl -F 'body=This issue was opened using GitHub API and Octopi' -F 'login=mylogin' -F 'token=mytoken' -F 'title=Sample issue' http://github.com/api/v2/issues/open/webbynode/api-labrat
47
+ ==================
48
+
49
+ == Anonymous Usage
50
+
51
+ This reflects the usage of the API to retrieve information, on a read-only mode where the user doesn't have to be authenticated.
6
52
 
7
53
  === Users API
8
54
 
@@ -44,6 +90,10 @@ Issues API integrated into the Repository object:
44
90
  issue = repo.issues.first
45
91
  puts "First open issue: #{issue.number} - #{issue.title} - Created at: #{issue.created_at}"
46
92
 
93
+ Single issue information:
94
+
95
+ issue = repo.issue(11)
96
+
47
97
  Commits API information from a Repository object:
48
98
 
49
99
  first_commit = repo.commits.first
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :major: 0
3
2
  :minor: 0
4
- :patch: 5
3
+ :patch: 6
4
+ :major: 0
data/lib/octopi.rb CHANGED
@@ -1,13 +1,30 @@
1
1
  require 'rubygems'
2
2
  require 'httparty'
3
+ require 'yaml'
3
4
  require 'pp'
4
5
 
5
6
  module Octopi
6
7
  class Api; end
7
8
  ANONYMOUS_API = Api.new
8
9
 
9
- def connect(login, token, &block)
10
- yield Api.new(login, token)
10
+ def authenticated_with(*args, &block)
11
+ opts = args.last.is_a?(Hash) ? args.last : {}
12
+ if opts[:config]
13
+ config = File.open(opts[:config]) { |yf| YAML::load(yf) }
14
+ raise "Missing config #{opts[:config]}" unless config
15
+
16
+ login = config["login"]
17
+ token = config["token"]
18
+ trace = config["trace"]
19
+ else
20
+ login, token = *args
21
+ end
22
+
23
+ puts "=> Trace on: #{trace}" if trace
24
+
25
+ api = Api.new(login, token)
26
+ api.trace = trace if trace
27
+ yield api
11
28
  end
12
29
 
13
30
  class Api
@@ -19,11 +36,17 @@ module Octopi
19
36
  }
20
37
  base_uri "http://github.com/api/v2"
21
38
 
22
- attr_accessor :format
39
+ attr_accessor :format, :login, :token, :trace, :read_only
23
40
 
24
- def initialize(login = nil, token = nil, format = "xml")
25
- self.class.default_params(:login => login, :token => token) if login
41
+ def initialize(login = nil, token = nil, format = "yaml")
26
42
  @format = format
43
+ @read_only = true
44
+
45
+ if login
46
+ @login = login
47
+ @token = token
48
+ @read_only = false
49
+ end
27
50
  end
28
51
 
29
52
  %w[keys emails].each do |action|
@@ -33,11 +56,22 @@ module Octopi
33
56
  end
34
57
 
35
58
  def user
36
- user_data = get("/user/show/#{self.class.default_params[:login]}")
59
+ user_data = get("/user/show/#{login}")
37
60
  raise "Unexpected response for user command" unless user_data and user_data['user']
38
61
  User.new(self, user_data['user'])
39
62
  end
40
63
 
64
+ def open_issue(user, repo, params)
65
+ Issue.open(user, repo, params, self)
66
+ end
67
+
68
+ def repository(name)
69
+ repo = Repository.find(login, name)
70
+ repo.api = self
71
+ repo
72
+ end
73
+ alias_method :repo, :repository
74
+
41
75
  def save(resource_path, data)
42
76
  traslate resource_path, data
43
77
  #still can't figure out on what format values are expected
@@ -55,39 +89,61 @@ module Octopi
55
89
  def get_raw(path, params)
56
90
  get(path, params, 'plain')
57
91
  end
92
+
93
+ def post(path, params = {}, format = "yaml")
94
+ trace "POST", path, params
95
+ submit(path, params, format) do |path, params, format|
96
+ resp = self.class.post "/#{format}#{path}", :query => params
97
+ resp
98
+ end
99
+ end
58
100
 
59
101
  private
60
- def get(path, params = {}, format = "yaml")
61
- params.each_pair do |k,v|
62
- path = path.gsub(":#{k.to_s}", v)
102
+ def submit(path, params = {}, format = "yaml", &block)
103
+ params.each_pair { |k,v| path = path.gsub(":#{k.to_s}", v) }
104
+ query = login ? { :login => login, :token => token } : {}
105
+ query.merge!(params)
106
+
107
+ if @trace
108
+ case @trace
109
+ when "curl"
110
+ query_trace = []
111
+ query.each_pair { |k,v| query_trace << "-F '#{k}=#{v}'" }
112
+ puts "===== [curl version]"
113
+ puts "curl #{query_trace.join(" ")} #{self.class.base_uri}#{path}"
114
+ puts "==================="
115
+ end
63
116
  end
64
- resp = self.class.get("/#{format}#{path}")
117
+
118
+ resp = yield(path, query, format)
119
+ raise APIError,
120
+ "GitHub returned status #{resp.code}" unless resp.code.to_i == 200
65
121
  # FIXME: This fails for showing raw Git data because that call returns
66
122
  # text/html as the content type. This issue has been reported.
67
123
  ctype = resp.headers['content-type'].first
68
124
  raise FormatError, [ctype, format] unless
69
125
  ctype.match(/^#{CONTENT_TYPE[format]};/)
70
- raise APIError,
71
- "GitHub returned status #{resp.code}" unless resp.code.to_i == 200
72
126
  if format == 'yaml' && resp['error']
73
127
  raise APIError, resp['error'].first['error']
74
128
  end
75
129
  resp
76
130
  end
131
+
132
+ def get(path, params = {}, format = "yaml")
133
+ trace "GET", path, params
134
+ submit(path, params, format) do |path, params, format|
135
+ self.class.get "/#{format}#{path}"
136
+ end
137
+ end
138
+
139
+ def trace(oper, url, params)
140
+ return unless @trace
141
+ par_str = " params: " + params.map { |p| "#{p[0]}=#{p[1]}" }.join(", ") if params and !params.empty?
142
+ puts "#{oper}: #{url}#{par_str}"
143
+ end
77
144
  end
78
-
79
- %w{base resource user tag repository issue file_object blob commit}.
80
- each{|f| require "#{File.dirname(__FILE__)}/octopi/#{f}"}
81
145
 
82
- class FormatError < StandardError
83
- def initialize(f)
84
- $stderr.puts "Got unexpected format (got #{f.first} for #{f.last})"
85
- end
86
- end
87
-
88
- class APIError < StandardError
89
- def initialize(m)
90
- $stderr.puts m
91
- end
92
- end
146
+ %w{error base resource user tag repository issue file_object blob commit}.
147
+ each{|f| require "#{File.dirname(__FILE__)}/octopi/#{f}"}
148
+
93
149
  end
data/lib/octopi/base.rb CHANGED
@@ -1,5 +1,29 @@
1
+ class String
2
+ def camel_case
3
+ self.gsub(/(^|_)(.)/) { $2.upcase }
4
+ end
5
+ end
1
6
  module Octopi
2
7
  class Base
8
+ VALID = {
9
+ :repo => {
10
+ # FIXME: API currently chokes on repository names containing periods,
11
+ # but presumably this will be fixed.
12
+ :pat => /^[a-z0-9_\.-]+$/,
13
+ :msg => "%s is an invalid repository name"},
14
+ :user => {
15
+ :pat => /^[A-Za-z0-9_\.-]+$/,
16
+ :msg => "%s is an invalid username"},
17
+ :file => {
18
+ :pat => /^[^ \/]+$/,
19
+ :msg => "%s is an invalid filename"},
20
+ :sha => {
21
+ :pat => /^[a-f0-9]{40}$/,
22
+ :msg => "%s is an invalid SHA hash"}
23
+ }
24
+
25
+ attr_accessor :api
26
+
3
27
  def initialize(api, hash)
4
28
  @api = api
5
29
  @keys = []
@@ -36,7 +60,8 @@ module Octopi
36
60
  def self.extract_user_repository(*args)
37
61
  opts = args.last.is_a?(Hash) ? args.pop : {}
38
62
  if opts.empty?
39
- user, repo = *args
63
+ user, repo = *args if args.length > 1
64
+ repo ||= args.first
40
65
  else
41
66
  opts[:repo] = opts[:repository] if opts[:repository]
42
67
  repo = args.pop || opts[:repo]
@@ -61,5 +86,17 @@ module Octopi
61
86
  v
62
87
  end
63
88
  end
89
+
90
+ def self.validate_args(spec)
91
+ m = caller[0].match(/\/([a-z0-9_]+)\.rb:\d+:in `([a-z_0-9]+)'/)
92
+ meth = m ? "#{m[1].camel_case}.#{m[2]}" : 'method'
93
+ raise ArgumentError, "Invalid spec" unless
94
+ spec.values.all? { |s| VALID.key? s }
95
+ errors = spec.reject{|arg, spec| arg.nil?}.
96
+ reject{|arg, spec| arg.to_s.match(VALID[spec][:pat])}.
97
+ map {|arg, spec| "Invalid argument '%s' for %s (%s)" %
98
+ [arg, meth, VALID[spec][:msg] % arg]}
99
+ raise ArgumentError, "\n" + errors.join("\n") unless errors.empty?
100
+ end
64
101
  end
65
- end
102
+ end
data/lib/octopi/blob.rb CHANGED
@@ -8,6 +8,7 @@ module Octopi
8
8
  def self.find(user, repo, sha, path=nil)
9
9
  user = user.login if user.is_a? User
10
10
  repo = repo.name if repo.is_a? Repository
11
+ self.class.validate_args(sha => :sha, user => :user, path => :file)
11
12
  if path
12
13
  super [user,repo,sha,path]
13
14
  else
@@ -17,4 +18,4 @@ module Octopi
17
18
  end
18
19
  end
19
20
  end
20
- end
21
+ end
data/lib/octopi/commit.rb CHANGED
@@ -24,6 +24,7 @@ module Octopi
24
24
  repo = args.first
25
25
  user ||= repo.owner if repo.is_a? Repository
26
26
  user, repo_name, opts = extract_user_repository(*args)
27
+ self.validate_args(user => :user, repo_name => :repo)
27
28
  branch = opts[:branch] || "master"
28
29
 
29
30
  commits = super user, repo_name, branch
@@ -40,6 +41,7 @@ module Octopi
40
41
  user, name, sha = *args
41
42
  user = user.login if user.is_a? User
42
43
  name = repo.name if name.is_a? Repository
44
+ self.validate_args(user => :user, name => :repo, sha => :sha)
43
45
  super [user, name, sha]
44
46
  end
45
47
  end
@@ -59,4 +61,4 @@ module Octopi
59
61
  parts.join('/')
60
62
  end
61
63
  end
62
- end
64
+ end
@@ -0,0 +1,15 @@
1
+ module Octopi
2
+
3
+ class FormatError < StandardError
4
+ def initialize(f)
5
+ $stderr.puts "Got unexpected format (got #{f.first} for #{f.last})"
6
+ end
7
+ end
8
+
9
+ class APIError < StandardError
10
+ def initialize(m)
11
+ $stderr.puts m
12
+ end
13
+ end
14
+
15
+ end
@@ -8,7 +8,8 @@ module Octopi
8
8
  def self.find(user, repo, sha)
9
9
  user = user.login if user.is_a? User
10
10
  repo = repo.name if repo.is_a? Repository
11
+ self.validate_args(sha => :sha, user => :user, repo => :repo)
11
12
  super [user,repo,sha]
12
13
  end
13
14
  end
14
- end
15
+ end
data/lib/octopi/issue.rb CHANGED
@@ -3,7 +3,7 @@ module Octopi
3
3
  include Resource
4
4
 
5
5
  find_path "/issues/list/:query"
6
- resource_path "/user/show/:id"
6
+ resource_path "/issues/show/:id"
7
7
 
8
8
  attr_accessor :repository
9
9
 
@@ -24,6 +24,7 @@ module Octopi
24
24
  def self.find_all(*args)
25
25
  repo = args.first
26
26
  user, repo_name, opts = extract_user_repository(*args)
27
+ validate_args(user => :user, repo_name => :repo)
27
28
  state = opts[:state] || "open"
28
29
  state.downcase! if state
29
30
 
@@ -36,15 +37,63 @@ module Octopi
36
37
 
37
38
  # TODO: Make find use hashes like find_all
38
39
  def self.find(*args)
39
- if args.last.is_a?(Issue)
40
- commit = args.pop
41
- super "#{issue.number}"
40
+ if args.length < 2
41
+ raise "Issue.find needs user, repository and issue number"
42
+ end
43
+
44
+ number = args.pop.to_i if args.last.respond_to?(:to_i)
45
+ number = args.pop if args.last.is_a?(Integer)
46
+
47
+ raise "Issue.find needs issue number as the last argument" unless number
48
+
49
+ if args.length > 1
50
+ user, repo = *args
42
51
  else
43
- user, name, number = *args
44
- user = user.login if user.is_a? User
45
- name = repo.name if name.is_a? Repository
46
- super user, name, number
52
+ repo = args.pop
53
+ raise "Issue.find needs at least a Repository object and issue number" unless repo.is_a? Repository
54
+ user, repo = repo.owner, repo.name
55
+ end
56
+
57
+ user, repo = extract_names(user, repo)
58
+ validate_args(user => :user, repo => :repo)
59
+ super user, repo, number
60
+ end
61
+
62
+ def self.open(user, repo, params, api = ANONYMOUS_API)
63
+ user, repo_name = extract_names(user, repo)
64
+ data = api.post("/issues/open/#{user}/#{repo_name}", params)
65
+ issue = new(api, data['issue'])
66
+ issue.repository = repo if repo.is_a? Repository
67
+ issue
68
+ end
69
+
70
+ def reopen(*args)
71
+ data = @api.post(command_path("reopen"))
72
+ end
73
+
74
+ def close(*args)
75
+ data = @api.post(command_path("close"))
76
+ end
77
+
78
+ def save
79
+ data = @api.post(command_path("edit"), { :title => self.title, :body => self.body })
80
+ end
81
+
82
+ %[add remove].each do |oper|
83
+ define_method("#{oper}_label") do |*labels|
84
+ labels.each do |label|
85
+ @api.post("#{prefix("label/#{oper}")}/#{label}/#{number}")
86
+ end
47
87
  end
48
88
  end
89
+
90
+ private
91
+ def prefix(command)
92
+ "/issues/#{command}/#{repository.owner}/#{repository.name}"
93
+ end
94
+
95
+ def command_path(command)
96
+ "#{prefix(command)}/#{number}"
97
+ end
49
98
  end
50
- end
99
+ end
@@ -18,13 +18,15 @@ module Octopi
18
18
 
19
19
  def self.find_by_user(user)
20
20
  user = user.login if user.is_a? User
21
+ self.validate_args(user => :user)
21
22
  find_plural(user, :resource)
22
23
  end
23
24
 
24
25
  def self.find(user, name)
25
26
  user = user.login if user.is_a? User
26
27
  name = repo.name if name.is_a? Repository
27
- super [user,name]
28
+ self.validate_args(user => :user, name => :repo)
29
+ super [user, name]
28
30
  end
29
31
 
30
32
  def self.find_all(*args)
@@ -33,6 +35,14 @@ module Octopi
33
35
  super args.join(" ").gsub(/ /,'+')
34
36
  end
35
37
 
38
+ def self.open_issue(args)
39
+ Issue.open(args[:user], args[:repo], args)
40
+ end
41
+
42
+ def open_issue(args)
43
+ Issue.open(self.owner, self, args, @api)
44
+ end
45
+
36
46
  def commits(branch = "master")
37
47
  Commit.find_all(self, :branch => branch)
38
48
  end
@@ -40,5 +50,9 @@ module Octopi
40
50
  def issues(state = "open")
41
51
  Issue.find_all(self, :state => state)
42
52
  end
53
+
54
+ def issue(number)
55
+ Issue.find(self, number)
56
+ end
43
57
  end
44
58
  end
@@ -28,7 +28,7 @@ module Octopi
28
28
  (@path_spec||={})[:resource] = path
29
29
  end
30
30
 
31
- def find(s)
31
+ def find(*s)
32
32
  s = s.join('/') if s.is_a? Array
33
33
  result = ANONYMOUS_API.find(path_for(:resource), @resource_name[:singular], s)
34
34
  key = result.keys.first
data/lib/octopi/tag.rb CHANGED
@@ -8,9 +8,10 @@ module Octopi
8
8
  def self.find(user, repo)
9
9
  user = user.login if user.is_a? User
10
10
  repo = repo.name if repo.is_a? Repository
11
+ self.validate_args(user => :user, repo => :repo)
11
12
  find_plural([user,repo,'tags'], :resource){
12
13
  |i| {:name => i.first, :hash => i.last }
13
14
  }
14
15
  end
15
16
  end
16
- end
17
+ end
data/lib/octopi/user.rb CHANGED
@@ -5,11 +5,22 @@ module Octopi
5
5
  find_path "/user/search/:query"
6
6
  resource_path "/user/show/:id"
7
7
 
8
+ def self.find(username)
9
+ self.validate_args(username => :user)
10
+ super username
11
+ end
12
+
13
+ def self.find_all(username)
14
+ self.validate_args(username => :user)
15
+ super username
16
+ end
17
+
8
18
  def repositories
9
19
  Repository.find_by_user(login)
10
20
  end
11
21
 
12
22
  def repository(name)
23
+ self.class.validate_args(name => :repo)
13
24
  Repository.find(login, name)
14
25
  end
15
26
 
@@ -35,4 +46,4 @@ module Octopi
35
46
  users
36
47
  end
37
48
  end
38
- end
49
+ end
data/test/octopi_test.rb CHANGED
@@ -1,7 +1,46 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class OctopiTest < Test::Unit::TestCase
4
- should "probably rename this file and start testing for real" do
5
- flunk "hey buddy, you should probably rename this file and start testing for real"
4
+ include Octopi
5
+
6
+ # TODO: Those tests are obviously brittle. Need to stub/mock it.
7
+
8
+ def assert_find_all(cls, check_method, repo, user)
9
+ repo_method = cls.resource_name(:plural)
10
+
11
+ item1 = cls.find_all(user.login, repo.name).first
12
+ item2 = cls.find_all(repo).first
13
+ item3 = repo.send(repo_method).first
14
+
15
+ assert_equal item1.send(check_method), item2.send(check_method)
16
+ assert_equal item1.send(check_method), item3.send(check_method)
17
+ end
18
+
19
+ def setup
20
+ @user = User.find("fcoury")
21
+ @repo = @user.repository("octopi")
22
+ @issue = @repo.issues.first
23
+ end
24
+
25
+ context Issue do
26
+ should "return the correct issue by number" do
27
+ assert_equal @issue.number, Issue.find(@repo, @issue.number).number
28
+ assert_equal @issue.number, Issue.find(@user, @repo, @issue.number).number
29
+ assert_equal @issue.number, Issue.find(@repo.owner, @repo.name, @issue.number).number
30
+ end
31
+
32
+ should "return the correct issue by using repo.issue number" do
33
+ assert_equal @issue.number, @repo.issue(@issue.number).number
34
+ end
35
+
36
+ should "fetch the same issue using different but equivalent find_all params" do
37
+ assert_find_all Issue, :number, @repo, @user
38
+ end
39
+ end
40
+
41
+ context Commit do
42
+ should "fetch the same commit using different but equivalent find_all params" do
43
+ assert_find_all Commit, :id, @repo, @user
44
+ end
6
45
  end
7
46
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fcoury-octopi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felipe Coury
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-04-20 00:00:00 -07:00
12
+ date: 2009-04-21 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -29,6 +29,7 @@ files:
29
29
  - lib/octopi/base.rb
30
30
  - lib/octopi/blob.rb
31
31
  - lib/octopi/commit.rb
32
+ - lib/octopi/error.rb
32
33
  - lib/octopi/file_object.rb
33
34
  - lib/octopi/issue.rb
34
35
  - lib/octopi/repository.rb
@@ -64,7 +65,7 @@ requirements: []
64
65
  rubyforge_project:
65
66
  rubygems_version: 1.2.0
66
67
  signing_key:
67
- specification_version: 3
68
+ specification_version: 2
68
69
  summary: A Ruby interface to GitHub API v2
69
70
  test_files: []
70
71