sclemmer-jira-ruby 0.1.12

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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +46 -0
  6. data/README.rdoc +309 -0
  7. data/Rakefile +28 -0
  8. data/example.rb +119 -0
  9. data/http-basic-example.rb +112 -0
  10. data/lib/jira.rb +33 -0
  11. data/lib/jira/base.rb +497 -0
  12. data/lib/jira/base_factory.rb +49 -0
  13. data/lib/jira/client.rb +165 -0
  14. data/lib/jira/has_many_proxy.rb +43 -0
  15. data/lib/jira/http_client.rb +69 -0
  16. data/lib/jira/http_error.rb +16 -0
  17. data/lib/jira/oauth_client.rb +84 -0
  18. data/lib/jira/railtie.rb +10 -0
  19. data/lib/jira/request_client.rb +18 -0
  20. data/lib/jira/resource/attachment.rb +12 -0
  21. data/lib/jira/resource/comment.rb +14 -0
  22. data/lib/jira/resource/component.rb +10 -0
  23. data/lib/jira/resource/field.rb +10 -0
  24. data/lib/jira/resource/filter.rb +15 -0
  25. data/lib/jira/resource/issue.rb +80 -0
  26. data/lib/jira/resource/issuetype.rb +10 -0
  27. data/lib/jira/resource/priority.rb +10 -0
  28. data/lib/jira/resource/project.rb +31 -0
  29. data/lib/jira/resource/status.rb +10 -0
  30. data/lib/jira/resource/transition.rb +33 -0
  31. data/lib/jira/resource/user.rb +14 -0
  32. data/lib/jira/resource/version.rb +10 -0
  33. data/lib/jira/resource/worklog.rb +16 -0
  34. data/lib/jira/tasks.rb +0 -0
  35. data/lib/jira/version.rb +3 -0
  36. data/lib/tasks/generate.rake +18 -0
  37. data/sclemmer-jira-ruby.gemspec +28 -0
  38. data/spec/integration/attachment_spec.rb +23 -0
  39. data/spec/integration/comment_spec.rb +55 -0
  40. data/spec/integration/component_spec.rb +42 -0
  41. data/spec/integration/field_spec.rb +35 -0
  42. data/spec/integration/issue_spec.rb +94 -0
  43. data/spec/integration/issuetype_spec.rb +26 -0
  44. data/spec/integration/priority_spec.rb +27 -0
  45. data/spec/integration/project_spec.rb +56 -0
  46. data/spec/integration/status_spec.rb +27 -0
  47. data/spec/integration/transition_spec.rb +52 -0
  48. data/spec/integration/user_spec.rb +25 -0
  49. data/spec/integration/version_spec.rb +43 -0
  50. data/spec/integration/worklog_spec.rb +55 -0
  51. data/spec/jira/base_factory_spec.rb +46 -0
  52. data/spec/jira/base_spec.rb +583 -0
  53. data/spec/jira/client_spec.rb +188 -0
  54. data/spec/jira/has_many_proxy_spec.rb +47 -0
  55. data/spec/jira/http_client_spec.rb +109 -0
  56. data/spec/jira/http_error_spec.rb +26 -0
  57. data/spec/jira/oauth_client_spec.rb +111 -0
  58. data/spec/jira/request_client_spec.rb +14 -0
  59. data/spec/jira/resource/attachment_spec.rb +20 -0
  60. data/spec/jira/resource/filter_spec.rb +97 -0
  61. data/spec/jira/resource/issue_spec.rb +123 -0
  62. data/spec/jira/resource/project_factory_spec.rb +13 -0
  63. data/spec/jira/resource/project_spec.rb +70 -0
  64. data/spec/jira/resource/worklog_spec.rb +24 -0
  65. data/spec/mock_responses/attachment/10000.json +20 -0
  66. data/spec/mock_responses/component.post.json +28 -0
  67. data/spec/mock_responses/component/10000.invalid.put.json +5 -0
  68. data/spec/mock_responses/component/10000.json +39 -0
  69. data/spec/mock_responses/component/10000.put.json +39 -0
  70. data/spec/mock_responses/field.json +32 -0
  71. data/spec/mock_responses/field/1.json +15 -0
  72. data/spec/mock_responses/issue.json +1108 -0
  73. data/spec/mock_responses/issue.post.json +5 -0
  74. data/spec/mock_responses/issue/10002.invalid.put.json +6 -0
  75. data/spec/mock_responses/issue/10002.json +126 -0
  76. data/spec/mock_responses/issue/10002.put.missing_field_update.json +6 -0
  77. data/spec/mock_responses/issue/10002/comment.json +65 -0
  78. data/spec/mock_responses/issue/10002/comment.post.json +29 -0
  79. data/spec/mock_responses/issue/10002/comment/10000.json +29 -0
  80. data/spec/mock_responses/issue/10002/comment/10000.put.json +29 -0
  81. data/spec/mock_responses/issue/10002/transitions.json +49 -0
  82. data/spec/mock_responses/issue/10002/transitions.post.json +1 -0
  83. data/spec/mock_responses/issue/10002/worklog.json +98 -0
  84. data/spec/mock_responses/issue/10002/worklog.post.json +30 -0
  85. data/spec/mock_responses/issue/10002/worklog/10000.json +31 -0
  86. data/spec/mock_responses/issue/10002/worklog/10000.put.json +30 -0
  87. data/spec/mock_responses/issuetype.json +42 -0
  88. data/spec/mock_responses/issuetype/5.json +8 -0
  89. data/spec/mock_responses/priority.json +42 -0
  90. data/spec/mock_responses/priority/1.json +8 -0
  91. data/spec/mock_responses/project.json +12 -0
  92. data/spec/mock_responses/project/SAMPLEPROJECT.issues.json +1108 -0
  93. data/spec/mock_responses/project/SAMPLEPROJECT.json +84 -0
  94. data/spec/mock_responses/status.json +37 -0
  95. data/spec/mock_responses/status/1.json +7 -0
  96. data/spec/mock_responses/user_username=admin.json +17 -0
  97. data/spec/mock_responses/version.post.json +7 -0
  98. data/spec/mock_responses/version/10000.invalid.put.json +5 -0
  99. data/spec/mock_responses/version/10000.json +11 -0
  100. data/spec/mock_responses/version/10000.put.json +7 -0
  101. data/spec/spec_helper.rb +22 -0
  102. data/spec/support/clients_helper.rb +16 -0
  103. data/spec/support/matchers/have_attributes.rb +11 -0
  104. data/spec/support/matchers/have_many.rb +9 -0
  105. data/spec/support/matchers/have_one.rb +5 -0
  106. data/spec/support/shared_examples/integration.rb +194 -0
  107. metadata +302 -0
@@ -0,0 +1,49 @@
1
+ module JIRA
2
+
3
+ # This is the base class for all the JIRA resource factory instances.
4
+ class BaseFactory
5
+
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ # Return the name of the class which this factory generates, i.e.
13
+ # JIRA::Resource::FooFactory creates JIRA::Resource::Foo instances.
14
+ def target_class
15
+ # Need to do a little bit of work here as Module.const_get doesn't work
16
+ # with nested class names, i.e. JIRA::Resource::Foo.
17
+ #
18
+ # So create a method chain from the class componenets. This code will
19
+ # unroll to:
20
+ # Module.const_get('JIRA').const_get('Resource').const_get('Foo')
21
+ #
22
+ target_class_name = self.class.name.sub(/Factory$/, '')
23
+ class_components = target_class_name.split('::')
24
+
25
+ class_components.inject(Module) do |mod, const_name|
26
+ mod.const_get(const_name)
27
+ end
28
+ end
29
+
30
+ def self.delegate_to_target_class(*method_names)
31
+ method_names.each do |method_name|
32
+ define_method method_name do |*args|
33
+ target_class.send(method_name, @client, *args)
34
+ end
35
+ end
36
+ end
37
+
38
+ # The priciple purpose of this class is to delegate methods to the corresponding
39
+ # non-factory class and automatically prepend the client argument to the argument
40
+ # list.
41
+ delegate_to_target_class :all, :find, :collection_path, :singular_path, :jql
42
+
43
+ # This method needs special handling as it has a default argument value
44
+ def build(attrs={})
45
+ target_class.build(@client, attrs)
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,165 @@
1
+ require 'json'
2
+ require 'forwardable'
3
+
4
+ module JIRA
5
+
6
+ # This class is the main access point for all JIRA::Resource instances.
7
+ #
8
+ # The client must be initialized with an options hash containing
9
+ # configuration options. The available options are:
10
+ #
11
+ # :site => 'http://localhost:2990',
12
+ # :context_path => '/jira',
13
+ # :signature_method => 'RSA-SHA1',
14
+ # :request_token_path => "/plugins/servlet/oauth/request-token",
15
+ # :authorize_path => "/plugins/servlet/oauth/authorize",
16
+ # :access_token_path => "/plugins/servlet/oauth/access-token",
17
+ # :private_key_file => "rsakey.pem",
18
+ # :rest_base_path => "/rest/api/2",
19
+ # :consumer_key => nil,
20
+ # :consumer_secret => nil,
21
+ # :ssl_verify_mode => OpenSSL::SSL::VERIFY_PEER,
22
+ # :use_ssl => true,
23
+ # :username => nil,
24
+ # :password => nil,
25
+ # :auth_type => :oauth
26
+ # :proxy_address => nil
27
+ # :proxy_port => nil
28
+ #
29
+ # See the JIRA::Base class methods for all of the available methods on these accessor
30
+ # objects.
31
+
32
+ class Client
33
+
34
+ extend Forwardable
35
+
36
+ # The OAuth::Consumer instance returned by the OauthClient
37
+ #
38
+ # The authenticated client instance returned by the respective client type
39
+ # (Oauth, Basic)
40
+ attr_accessor :consumer, :request_client
41
+
42
+ # The configuration options for this client instance
43
+ attr_reader :options
44
+
45
+ def_delegators :@request_client, :init_access_token, :set_access_token, :set_request_token, :request_token, :access_token
46
+
47
+ DEFAULT_OPTIONS = {
48
+ :site => 'http://localhost:2990',
49
+ :context_path => '/jira',
50
+ :rest_base_path => "/rest/api/2",
51
+ :ssl_verify_mode => OpenSSL::SSL::VERIFY_PEER,
52
+ :use_ssl => true,
53
+ :auth_type => :oauth
54
+ }
55
+
56
+ def initialize(options={})
57
+ options = DEFAULT_OPTIONS.merge(options)
58
+ @options = options
59
+ @options[:rest_base_path] = @options[:context_path] + @options[:rest_base_path]
60
+
61
+ case options[:auth_type]
62
+ when :oauth
63
+ @request_client = OauthClient.new(@options)
64
+ @consumer = @request_client.consumer
65
+ when :basic
66
+ @request_client = HttpClient.new(@options)
67
+ end
68
+
69
+ @options.freeze
70
+ end
71
+
72
+ def Project # :nodoc:
73
+ JIRA::Resource::ProjectFactory.new(self)
74
+ end
75
+
76
+ def Issue # :nodoc:
77
+ JIRA::Resource::IssueFactory.new(self)
78
+ end
79
+
80
+ def Filter # :nodoc:
81
+ JIRA::Resource::FilterFactory.new(self)
82
+ end
83
+
84
+ def Component # :nodoc:
85
+ JIRA::Resource::ComponentFactory.new(self)
86
+ end
87
+
88
+ def User # :nodoc:
89
+ JIRA::Resource::UserFactory.new(self)
90
+ end
91
+
92
+ def Issuetype # :nodoc:
93
+ JIRA::Resource::IssuetypeFactory.new(self)
94
+ end
95
+
96
+ def Priority # :nodoc:
97
+ JIRA::Resource::PriorityFactory.new(self)
98
+ end
99
+
100
+ def Status # :nodoc:
101
+ JIRA::Resource::StatusFactory.new(self)
102
+ end
103
+
104
+ def Comment # :nodoc:
105
+ JIRA::Resource::CommentFactory.new(self)
106
+ end
107
+
108
+ def Attachment # :nodoc:
109
+ JIRA::Resource::AttachmentFactory.new(self)
110
+ end
111
+
112
+ def Worklog # :nodoc:
113
+ JIRA::Resource::WorklogFactory.new(self)
114
+ end
115
+
116
+ def Version # :nodoc:
117
+ JIRA::Resource::VersionFactory.new(self)
118
+ end
119
+
120
+ def Transition # :nodoc:
121
+ JIRA::Resource::TransitionFactory.new(self)
122
+ end
123
+
124
+ def Field # :nodoc:
125
+ JIRA::Resource::FieldFactory.new(self)
126
+ end
127
+
128
+ # HTTP methods without a body
129
+ def delete(path, headers = {})
130
+ request(:delete, path, nil, merge_default_headers(headers))
131
+ end
132
+
133
+ def get(path, headers = {})
134
+ request(:get, path, nil, merge_default_headers(headers))
135
+ end
136
+
137
+ def head(path, headers = {})
138
+ request(:head, path, nil, merge_default_headers(headers))
139
+ end
140
+
141
+ # HTTP methods with a body
142
+ def post(path, body = '', headers = {})
143
+ headers = {'Content-Type' => 'application/json'}.merge(headers)
144
+ request(:post, path, body, merge_default_headers(headers))
145
+ end
146
+
147
+ def put(path, body = '', headers = {})
148
+ headers = {'Content-Type' => 'application/json'}.merge(headers)
149
+ request(:put, path, body, merge_default_headers(headers))
150
+ end
151
+
152
+ # Sends the specified HTTP request to the REST API through the
153
+ # appropriate method (oauth, basic).
154
+ def request(http_method, path, body = '', headers={})
155
+ @request_client.request(http_method, path, body, headers)
156
+ end
157
+
158
+ protected
159
+
160
+ def merge_default_headers(headers)
161
+ {'Accept' => 'application/json'}.merge(headers)
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,43 @@
1
+ #
2
+ # Whenever a collection from a has_many relationship is accessed, an instance
3
+ # of this class is returned. This instance wraps the Array of instances in
4
+ # the collection with an extra build method, which allows new instances to be
5
+ # built on the collection with the correct properties.
6
+ #
7
+ # In practice, instances of this class behave exactly like an Array.
8
+ #
9
+ class JIRA::HasManyProxy
10
+
11
+ attr_reader :target_class, :parent
12
+ attr_accessor :collection
13
+
14
+ def initialize(parent, target_class, collection = [])
15
+ @parent = parent
16
+ @target_class = target_class
17
+ @collection = collection
18
+ end
19
+
20
+ # Builds an instance of this class with the correct parent.
21
+ # For example, issue.comments.build(attrs) will initialize a
22
+ # comment as follows:
23
+ #
24
+ # JIRA::Resource::Comment.new(issue.client,
25
+ # :attrs => attrs,
26
+ # :issue => issue)
27
+ def build(attrs = {})
28
+ resource = target_class.new(parent.client, :attrs => attrs, parent.to_sym => parent)
29
+ collection << resource
30
+ resource
31
+ end
32
+
33
+ # Forces an HTTP request to fetch all instances of the target class that
34
+ # are associated with the parent
35
+ def all
36
+ target_class.all(parent.client, parent.to_sym => parent)
37
+ end
38
+
39
+ # Delegate any missing methods to the collection that this proxy wraps
40
+ def method_missing(method_name, *args, &block)
41
+ collection.send(method_name, *args, &block)
42
+ end
43
+ end
@@ -0,0 +1,69 @@
1
+ require 'json'
2
+ require 'net/https'
3
+ require 'cgi/cookie'
4
+
5
+ module JIRA
6
+ class HttpClient < RequestClient
7
+
8
+ DEFAULT_OPTIONS = {
9
+ :username => '',
10
+ :password => ''
11
+ }
12
+
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = DEFAULT_OPTIONS.merge(options)
17
+ @cookies = {}
18
+ end
19
+
20
+ def make_request(http_method, path, body='', headers={})
21
+ request = Net::HTTP.const_get(http_method.to_s.capitalize).new(path, headers)
22
+ request.body = body unless body.nil?
23
+ add_cookies(request) if options[:use_cookies]
24
+ request.basic_auth(@options[:username], @options[:password])
25
+ response = basic_auth_http_conn.request(request)
26
+ store_cookies(response) if options[:use_cookies]
27
+ response
28
+ end
29
+
30
+ def basic_auth_http_conn
31
+ http_conn(uri)
32
+ end
33
+
34
+ def http_conn(uri)
35
+ if @options[:proxy_address]
36
+ http_class = Net::HTTP::Proxy(@options[:proxy_address], @options[:proxy_port] ? @options[:proxy_port] : 80)
37
+ else
38
+ http_class = Net::HTTP
39
+ end
40
+ http_conn = http_class.new(uri.host, uri.port)
41
+ http_conn.use_ssl = @options[:use_ssl]
42
+ http_conn.verify_mode = @options[:ssl_verify_mode]
43
+ http_conn
44
+ end
45
+
46
+ def uri
47
+ uri = URI.parse(@options[:site])
48
+ end
49
+
50
+ private
51
+
52
+ def store_cookies(response)
53
+ cookies = response.get_fields('set-cookie')
54
+ if cookies
55
+ cookies.each do |cookie|
56
+ data = CGI::Cookie.parse(cookie)
57
+ data.delete('Path')
58
+ @cookies.merge!(data)
59
+ end
60
+ end
61
+ end
62
+
63
+ def add_cookies(request)
64
+ cookie_array = @cookies.values.map { |cookie| cookie.to_s }
65
+ request.add_field('Cookie', cookie_array.join('; ')) if cookie_array.any?
66
+ request
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,16 @@
1
+ require 'forwardable'
2
+ module JIRA
3
+
4
+ class HTTPError < StandardError
5
+ extend Forwardable
6
+
7
+ def_instance_delegators :@response, :message, :code
8
+ attr_reader :response
9
+
10
+ def initialize(response)
11
+ @response = response
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,84 @@
1
+ require 'oauth'
2
+ require 'json'
3
+ require 'forwardable'
4
+
5
+ module JIRA
6
+ class OauthClient < RequestClient
7
+
8
+ DEFAULT_OPTIONS = {
9
+ :signature_method => 'RSA-SHA1',
10
+ :request_token_path => "/plugins/servlet/oauth/request-token",
11
+ :authorize_path => "/plugins/servlet/oauth/authorize",
12
+ :access_token_path => "/plugins/servlet/oauth/access-token",
13
+ :private_key_file => "rsakey.pem",
14
+ :consumer_key => nil,
15
+ :consumer_secret => nil
16
+ }
17
+
18
+ # This exception is thrown when the client is used before the OAuth access token
19
+ # has been initialized.
20
+ class UninitializedAccessTokenError < StandardError
21
+ def message
22
+ "init_access_token must be called before using the client"
23
+ end
24
+ end
25
+
26
+ extend Forwardable
27
+
28
+ attr_accessor :consumer
29
+ attr_reader :options
30
+
31
+ def_instance_delegators :@consumer, :key, :secret, :get_request_token
32
+
33
+ def initialize(options)
34
+ @options = DEFAULT_OPTIONS.merge(options)
35
+ @consumer = init_oauth_consumer(@options)
36
+ end
37
+
38
+ def init_oauth_consumer(options)
39
+ @options[:request_token_path] = @options[:context_path] + @options[:request_token_path]
40
+ @options[:authorize_path] = @options[:context_path] + @options[:authorize_path]
41
+ @options[:access_token_path] = @options[:context_path] + @options[:access_token_path]
42
+ OAuth::Consumer.new(@options[:consumer_key],@options[:consumer_secret],@options)
43
+ end
44
+
45
+ # Returns the current request token if it is set, else it creates
46
+ # and sets a new token.
47
+ def request_token
48
+ @request_token ||= get_request_token
49
+ end
50
+
51
+ # Sets the request token from a given token and secret.
52
+ def set_request_token(token, secret)
53
+ @request_token = OAuth::RequestToken.new(@consumer, token, secret)
54
+ end
55
+
56
+ # Initialises and returns a new access token from the params hash
57
+ # returned by the OAuth transaction.
58
+ def init_access_token(params)
59
+ @access_token = request_token.get_access_token(params)
60
+ end
61
+
62
+ # Sets the access token from a preexisting token and secret.
63
+ def set_access_token(token, secret)
64
+ @access_token = OAuth::AccessToken.new(@consumer, token, secret)
65
+ end
66
+
67
+ # Returns the current access token. Raises an
68
+ # JIRA::Client::UninitializedAccessTokenError exception if it is not set.
69
+ def access_token
70
+ raise UninitializedAccessTokenError.new unless @access_token
71
+ @access_token
72
+ end
73
+
74
+ def make_request(http_method, path, body='', headers={})
75
+ case http_method
76
+ when :delete, :get, :head
77
+ response = access_token.send http_method, path, headers
78
+ when :post, :put
79
+ response = access_token.send http_method, path, body, headers
80
+ end
81
+ response
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,10 @@
1
+ require 'jira'
2
+ require 'rails'
3
+
4
+ module JIRA
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'tasks/generate.rake'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ require 'oauth'
2
+ require 'json'
3
+ require 'net/https'
4
+
5
+ module JIRA
6
+ class RequestClient
7
+
8
+ # Returns the response if the request was successful (HTTP::2xx) and
9
+ # raises a JIRA::HTTPError if it was not successful, with the response
10
+ # attached.
11
+
12
+ def request(*args)
13
+ response = make_request(*args)
14
+ raise HTTPError.new(response) unless response.kind_of?(Net::HTTPSuccess)
15
+ response
16
+ end
17
+ end
18
+ end