jira-ruby 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/Gemfile +5 -0
- data/README.markdown +81 -0
- data/Rakefile +9 -0
- data/example.rb +66 -0
- data/jira-ruby.gemspec +28 -0
- data/lib/jira.rb +12 -0
- data/lib/jira/client.rb +105 -0
- data/lib/jira/resource/base.rb +148 -0
- data/lib/jira/resource/base_factory.rb +44 -0
- data/lib/jira/resource/component.rb +15 -0
- data/lib/jira/resource/http_error.rb +17 -0
- data/lib/jira/resource/issue.rb +15 -0
- data/lib/jira/resource/project.rb +9 -0
- data/lib/jira/tasks.rb +0 -0
- data/lib/jira/version.rb +3 -0
- data/lib/tasks/generate.rake +16 -0
- data/spec/integration/component_spec.rb +70 -0
- data/spec/integration/issue_spec.rb +72 -0
- data/spec/integration/project_spec.rb +48 -0
- data/spec/jira/client_spec.rb +157 -0
- data/spec/jira/resource/base_factory_spec.rb +36 -0
- data/spec/jira/resource/base_spec.rb +292 -0
- data/spec/jira/resource/http_error_spec.rb +25 -0
- data/spec/jira/resource/project_factory_spec.rb +13 -0
- data/spec/mock_responses/component.post.json +28 -0
- data/spec/mock_responses/component/10000.json +39 -0
- data/spec/mock_responses/component/10000.put.json +39 -0
- data/spec/mock_responses/issue.post.json +5 -0
- data/spec/mock_responses/issue/10002.json +114 -0
- data/spec/mock_responses/project.json +12 -0
- data/spec/mock_responses/project/SAMPLEPROJECT.json +70 -0
- data/spec/spec_helper.rb +26 -0
- metadata +148 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
Jira 5 API Gem
|
2
|
+
==============
|
3
|
+
|
4
|
+
Links to JIRA REST API documentation
|
5
|
+
------------------------------------
|
6
|
+
* [Overview](https://developer.atlassian.com/display/JIRADEV/JIRA+REST+APIs)
|
7
|
+
* [Reference](http://docs.atlassian.com/jira/REST/5.0-rc1/)
|
8
|
+
|
9
|
+
|
10
|
+
Setting up the JIRA SDK
|
11
|
+
-----------------------
|
12
|
+
On Mac OS,
|
13
|
+
|
14
|
+
brew install atlassian-plugin-sdk
|
15
|
+
|
16
|
+
Otherwise:
|
17
|
+
|
18
|
+
* Download the SDK from https://developer.atlassian.com/ (You will need
|
19
|
+
an Atlassian login for this)
|
20
|
+
* Unpack the dowloaded archive
|
21
|
+
* From within the archive directory, run:
|
22
|
+
|
23
|
+
./bin/atlas-run-standalone --product jira --version 5.0-rc2
|
24
|
+
|
25
|
+
Once this is running, you should be able to connect to
|
26
|
+
[http://localhost:2990/] and login to the JIRA admin system using `admin:admin`
|
27
|
+
|
28
|
+
You'll need to create a dummy project and probably some issues to test using
|
29
|
+
this library.
|
30
|
+
|
31
|
+
Configuring JIRA to use OAuth
|
32
|
+
-----------------------------
|
33
|
+
From the Jira API tutorial
|
34
|
+
|
35
|
+
> The first step is to register a new consumer in JIRA. This is done through
|
36
|
+
> the Application Links administration screens in JIRA. Create a new
|
37
|
+
> Application Link.
|
38
|
+
> [Administration/Plugins/Application Links](http://localhost:2990/jira/plugins/servlet/applinks/listApplicationLinks)
|
39
|
+
>
|
40
|
+
> When creating the Application Link use a placeholder URL or the correct URL
|
41
|
+
> to your client (e.g. `http://localhost:3000`), if your client can be reached
|
42
|
+
> via HTTP and choose the Generic Application type. After this Application Link
|
43
|
+
> has been created, edit the configuration and go to the incoming
|
44
|
+
> authentication configuration screen and select OAuth. Enter in this the
|
45
|
+
> public key and the consumer key which your client will use when making
|
46
|
+
> requests to JIRA.
|
47
|
+
|
48
|
+
This public key and consumer key will need to be generated by the Gem user, using OpenSSL
|
49
|
+
or similar to generate the public key and the provided rake task to generate the consumer
|
50
|
+
key.
|
51
|
+
|
52
|
+
> After you have entered all the information click OK and ensure OAuth authentication is
|
53
|
+
> enabled.
|
54
|
+
|
55
|
+
Using the API Gem in your application
|
56
|
+
-------------------------------------
|
57
|
+
The JiraApi gem requires the consumer key and public certificate file (which are generated in their respective rake tasks) to initialize an access token for using the Jira API. These two pieces of information are applied globally to your application, with separate JiraApi::Client objects created on a per-user basis.
|
58
|
+
|
59
|
+
An example initializer which sets the key and certificate filename in a pair of globals is shown below, myapp/config/initializers/jira\_api.rb:
|
60
|
+
|
61
|
+
$CONSUMER_KEY = 'cbaec507669c65979b6b6eefdb1c5bb0' #Your consumer key. Can be generated by rake jira_api:generate_public_cert
|
62
|
+
$PUBLIC_CERT_FILE = 'rsakey.pem' #Location of the Private Key File (generated by rake jira_api:generate_public_cert
|
63
|
+
|
64
|
+
This allows acces to the variables when a session is being set up.
|
65
|
+
The following sample controller shows how to set up and initialize an access token for a particular user session.
|
66
|
+
(Note that the callback url is defined in the Jira application link interface, and can be placed wherever suits you best in your application. The session#callback method is simply an example)
|
67
|
+
|
68
|
+
\#TODO cannot pass params hash straight into init\_access\_token method - errors with a missing parameter exception
|
69
|
+
|
70
|
+
class SessionsController < ApplicationController
|
71
|
+
def create
|
72
|
+
session[:client] = JiraApi::Client.new($CONSUMER_KEY, '', :private_key_file => $PUBLIC_CERT_FILE)
|
73
|
+
session[:request_token] = session[:client].request_token #Generate the request token
|
74
|
+
redirect_to session[:request_token].authorize_url #Redirect to Jira to authorize the token
|
75
|
+
end
|
76
|
+
|
77
|
+
def callback
|
78
|
+
session[:client].init_access_token(:oauth_verifier => params[:oauth_verifier]) #Initialize the access token
|
79
|
+
redirect_to root_url #Redirect to the desired page after initializing the access token
|
80
|
+
end
|
81
|
+
end
|
data/Rakefile
ADDED
data/example.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require './lib/jira'
|
3
|
+
|
4
|
+
options = {
|
5
|
+
:private_key_file => "rsakey.pem"
|
6
|
+
}
|
7
|
+
|
8
|
+
CONSUMER_KEY = 'test'
|
9
|
+
|
10
|
+
client = Jira::Client.new(CONSUMER_KEY, '', options)
|
11
|
+
|
12
|
+
if ARGV.length == 0
|
13
|
+
# If not passed any command line arguments, open a browser and prompt the
|
14
|
+
# user for the OAuth verifier.
|
15
|
+
request_token = client.request_token
|
16
|
+
puts "Opening #{request_token.authorize_url}"
|
17
|
+
system "open #{request_token.authorize_url}"
|
18
|
+
|
19
|
+
puts "Enter the oauth_verifier: "
|
20
|
+
oauth_verifier = gets.strip
|
21
|
+
|
22
|
+
access_token = client.init_access_token(:oauth_verifier => oauth_verifier)
|
23
|
+
puts "Access token: #{access_token.token} secret: #{access_token.secret}"
|
24
|
+
elsif ARGV.length == 2
|
25
|
+
# Otherwise assume the arguments are a previous access token and secret.
|
26
|
+
access_token = client.set_access_token(ARGV[0], ARGV[1])
|
27
|
+
else
|
28
|
+
# Script must be passed 0 or 2 arguments
|
29
|
+
raise "Usage: #{$0} [ token secret ]"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Show all projects
|
33
|
+
projects = client.Project.all
|
34
|
+
|
35
|
+
projects.each do |project|
|
36
|
+
puts "Project -> key: #{project.key}, name: #{project.name}"
|
37
|
+
end
|
38
|
+
issue = client.Issue.find('SAMPLEPROJECT-1')
|
39
|
+
pp issue
|
40
|
+
|
41
|
+
# # Find a specific project by key
|
42
|
+
# # ------------------------------
|
43
|
+
# project = client.Project.find('SAMPLEPROJECT')
|
44
|
+
# pp project
|
45
|
+
#
|
46
|
+
# # Delete an issue
|
47
|
+
# # ---------------
|
48
|
+
# issue = client.Issue.find('SAMPLEPROJECT-2')
|
49
|
+
# if issue.delete
|
50
|
+
# puts "Delete of issue SAMPLEPROJECT-2 sucessful"
|
51
|
+
# else
|
52
|
+
# puts "Delete of issue SAMPLEPROJECT-2 failed"
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# # Create an issue
|
56
|
+
# # ---------------
|
57
|
+
# issue = client.Issue.build
|
58
|
+
# issue.save({"fields"=>{"summary"=>"blarg from in example.rb","project"=>{"id"=>"10001"},"issuetype"=>{"id"=>"3"}}})
|
59
|
+
# issue.fetch
|
60
|
+
# pp issue
|
61
|
+
#
|
62
|
+
# Update an issue
|
63
|
+
# ---------------
|
64
|
+
# issue = client.Issue.find("10002")
|
65
|
+
# issue.save({"fields"=>{"summary"=>"EVEN MOOOOOOARRR NINJAAAA!"}})
|
66
|
+
# pp issue
|
data/jira-ruby.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "jira/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "jira-ruby"
|
7
|
+
s.version = Jira::VERSION
|
8
|
+
s.authors = ["Trineo Ltd"]
|
9
|
+
s.homepage = "http://trineo.co.nz"
|
10
|
+
s.summary = %q{Ruby Gem for use with the Atlassian Jira 5 REST API}
|
11
|
+
s.description = %q{API for Jira 5}
|
12
|
+
|
13
|
+
s.rubyforge_project = "jira-ruby"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# specify any dependencies here; for example:
|
21
|
+
# s.add_development_dependency "rspec"
|
22
|
+
# s.add_runtime_dependency "rest-client"
|
23
|
+
s.add_runtime_dependency "oauth"
|
24
|
+
s.add_development_dependency "oauth"
|
25
|
+
s.add_runtime_dependency "railties"
|
26
|
+
s.add_development_dependency "railties"
|
27
|
+
s.add_development_dependency "webmock"
|
28
|
+
end
|
data/lib/jira.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f } if defined?(Rake)
|
2
|
+
|
3
|
+
$: << File.expand_path(File.dirname(__FILE__))
|
4
|
+
require 'jira/resource/base'
|
5
|
+
require 'jira/resource/base_factory'
|
6
|
+
require 'jira/resource/http_error'
|
7
|
+
|
8
|
+
require 'jira/resource/issue'
|
9
|
+
require 'jira/resource/project'
|
10
|
+
require 'jira/resource/component'
|
11
|
+
|
12
|
+
require 'jira/client'
|
data/lib/jira/client.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
require 'json'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Jira
|
6
|
+
class Client
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
class UninitializedAccessTokenError < StandardError
|
11
|
+
def message
|
12
|
+
"init_access_token must be called before using the client"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :consumer
|
17
|
+
attr_reader :options
|
18
|
+
delegate [:key, :secret, :get_request_token] => :consumer
|
19
|
+
|
20
|
+
DEFAULT_OPTIONS = {
|
21
|
+
:site => 'http://localhost:2990',
|
22
|
+
:signature_method => 'RSA-SHA1',
|
23
|
+
:request_token_path => "/jira/plugins/servlet/oauth/request-token",
|
24
|
+
:authorize_path => "/jira/plugins/servlet/oauth/authorize",
|
25
|
+
:access_token_path => "/jira/plugins/servlet/oauth/access-token",
|
26
|
+
:private_key_file => "rsakey.pem",
|
27
|
+
:rest_base_path => "/jira/rest/api/2"
|
28
|
+
}
|
29
|
+
|
30
|
+
def initialize(consumer_key, consumer_secret, options={})
|
31
|
+
options = DEFAULT_OPTIONS.merge(options)
|
32
|
+
|
33
|
+
@options = options
|
34
|
+
@options.freeze
|
35
|
+
@consumer = OAuth::Consumer.new(consumer_key,consumer_secret,options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def Project
|
39
|
+
Jira::Resource::ProjectFactory.new(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def Issue
|
43
|
+
Jira::Resource::IssueFactory.new(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
def Component
|
47
|
+
Jira::Resource::ComponentFactory.new(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def request_token
|
51
|
+
@request_token ||= get_request_token
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_request_token(token, secret)
|
55
|
+
@request_token = OAuth::RequestToken.new(@consumer, token, secret)
|
56
|
+
end
|
57
|
+
|
58
|
+
def init_access_token(params)
|
59
|
+
@access_token = request_token.get_access_token(params)
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_access_token(token, secret)
|
63
|
+
@access_token = OAuth::AccessToken.new(@consumer, token, secret)
|
64
|
+
end
|
65
|
+
|
66
|
+
def access_token
|
67
|
+
raise UninitializedAccessTokenError.new unless @access_token
|
68
|
+
@access_token
|
69
|
+
end
|
70
|
+
|
71
|
+
# HTTP methods without a body
|
72
|
+
def delete(path, headers = {})
|
73
|
+
request(:delete, path, merge_default_headers(headers))
|
74
|
+
end
|
75
|
+
def get(path, headers = {})
|
76
|
+
request(:get, path, merge_default_headers(headers))
|
77
|
+
end
|
78
|
+
def head(path, headers = {})
|
79
|
+
request(:head, path, merge_default_headers(headers))
|
80
|
+
end
|
81
|
+
|
82
|
+
# HTTP methods with a body
|
83
|
+
def post(path, body = '', headers = {})
|
84
|
+
headers = {'Content-Type' => 'application/json'}.merge(headers)
|
85
|
+
request(:post, path, body, merge_default_headers(headers))
|
86
|
+
end
|
87
|
+
def put(path, body = '', headers = {})
|
88
|
+
headers = {'Content-Type' => 'application/json'}.merge(headers)
|
89
|
+
request(:put, path, body, merge_default_headers(headers))
|
90
|
+
end
|
91
|
+
|
92
|
+
def request(http_method, path, *arguments)
|
93
|
+
response = access_token.request(http_method, path, *arguments)
|
94
|
+
raise Resource::HTTPError.new(response) unless response.kind_of?(Net::HTTPSuccess)
|
95
|
+
response
|
96
|
+
end
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
def merge_default_headers(headers)
|
101
|
+
{'Accept' => 'application/json'}.merge(headers)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Jira
|
2
|
+
module Resource
|
3
|
+
|
4
|
+
class Base
|
5
|
+
|
6
|
+
attr_reader :client
|
7
|
+
attr_accessor :expanded, :deleted, :attrs
|
8
|
+
alias :expanded? :expanded
|
9
|
+
alias :deleted? :deleted
|
10
|
+
|
11
|
+
def initialize(client, options = {})
|
12
|
+
@client = client
|
13
|
+
@attrs = options[:attrs] || {}
|
14
|
+
@expanded = options[:expanded] || false
|
15
|
+
@deleted = false
|
16
|
+
end
|
17
|
+
|
18
|
+
# The class methods are never called directly, they are always
|
19
|
+
# invoked from a BaseFactory subclass instance.
|
20
|
+
def self.all(client)
|
21
|
+
response = client.get(rest_base_path(client))
|
22
|
+
json = parse_json(response.body)
|
23
|
+
json.map do |attrs|
|
24
|
+
self.new(client, :attrs => attrs)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find(client, key)
|
29
|
+
instance = self.new(client)
|
30
|
+
instance.attrs[key_attribute.to_s] = key
|
31
|
+
instance.fetch
|
32
|
+
instance
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.build(client, attrs)
|
36
|
+
self.new(client, :attrs => attrs)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.rest_base_path(client)
|
40
|
+
client.options[:rest_base_path] + '/' + self.endpoint_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.endpoint_name
|
44
|
+
self.name.split('::').last.downcase
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.key_attribute
|
48
|
+
:key
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.parse_json(string)
|
52
|
+
JSON.parse(string)
|
53
|
+
end
|
54
|
+
|
55
|
+
def respond_to?(method_name)
|
56
|
+
if attrs.keys.include? method_name.to_s
|
57
|
+
true
|
58
|
+
else
|
59
|
+
super(method_name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def method_missing(method_name, *args, &block)
|
64
|
+
if attrs.keys.include? method_name.to_s
|
65
|
+
attrs[method_name.to_s]
|
66
|
+
else
|
67
|
+
super(method_name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def rest_base_path
|
72
|
+
# Just proxy this to the class method
|
73
|
+
self.class.rest_base_path(client)
|
74
|
+
end
|
75
|
+
|
76
|
+
def fetch(reload = false)
|
77
|
+
return if expanded? && !reload
|
78
|
+
response = client.get(url)
|
79
|
+
set_attrs_from_response(response)
|
80
|
+
@expanded = true
|
81
|
+
end
|
82
|
+
|
83
|
+
def save(attrs)
|
84
|
+
http_method = new_record? ? :post : :put
|
85
|
+
response = client.send(http_method, url, attrs.to_json)
|
86
|
+
set_attrs(attrs, false)
|
87
|
+
set_attrs_from_response(response)
|
88
|
+
@expanded = false
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def set_attrs_from_response(response)
|
93
|
+
unless response.body.nil? or response.body.length < 2
|
94
|
+
json = self.class.parse_json(response.body)
|
95
|
+
set_attrs(json)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Set the current attributes from a hash. If clobber is true, any existing
|
100
|
+
# hash values will be clobbered by the new hash, otherwise the hash will
|
101
|
+
# be deeply merged into attrs. The target paramater is for internal use only
|
102
|
+
# and should not be used.
|
103
|
+
def set_attrs(hash, clobber=true, target = nil)
|
104
|
+
target ||= @attrs
|
105
|
+
if clobber
|
106
|
+
target.merge!(hash)
|
107
|
+
hash
|
108
|
+
else
|
109
|
+
hash.each do |k, v|
|
110
|
+
if v.is_a?(Hash)
|
111
|
+
set_attrs(v, clobber, target[k])
|
112
|
+
else
|
113
|
+
target[k] = v
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def delete
|
120
|
+
client.delete(url)
|
121
|
+
@deleted = true
|
122
|
+
end
|
123
|
+
|
124
|
+
def url
|
125
|
+
if @attrs['self']
|
126
|
+
@attrs['self']
|
127
|
+
elsif @attrs[self.class.key_attribute.to_s]
|
128
|
+
rest_base_path + "/" + @attrs[self.class.key_attribute.to_s].to_s
|
129
|
+
else
|
130
|
+
rest_base_path
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_s
|
135
|
+
"#<#{self.class.name}:#{object_id} @attrs=#{@attrs.inspect}>"
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_json
|
139
|
+
attrs.to_json
|
140
|
+
end
|
141
|
+
|
142
|
+
def new_record?
|
143
|
+
@attrs['id'].nil?
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|