jiraby 0.0.1

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 (51) hide show
  1. data/.gitignore +9 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +4 -0
  4. data/.yardopts +8 -0
  5. data/Gemfile +7 -0
  6. data/README.md +132 -0
  7. data/Rakefile +5 -0
  8. data/docs/development.md +20 -0
  9. data/docs/history.md +5 -0
  10. data/docs/ideas.md +54 -0
  11. data/docs/index.md +11 -0
  12. data/docs/usage.md +64 -0
  13. data/jiraby.gemspec +31 -0
  14. data/lib/jiraby.rb +8 -0
  15. data/lib/jiraby/entity.rb +21 -0
  16. data/lib/jiraby/exceptions.rb +8 -0
  17. data/lib/jiraby/issue.rb +109 -0
  18. data/lib/jiraby/jira.rb +319 -0
  19. data/lib/jiraby/json_resource.rb +136 -0
  20. data/lib/jiraby/project.rb +19 -0
  21. data/spec/data/field.json +32 -0
  22. data/spec/data/issue_10002.json +187 -0
  23. data/spec/data/issue_createmeta.json +35 -0
  24. data/spec/data/jira_issues.rb +265 -0
  25. data/spec/data/jira_projects.rb +117 -0
  26. data/spec/data/project_TST.json +97 -0
  27. data/spec/data/search_results.json +26 -0
  28. data/spec/entity_spec.rb +20 -0
  29. data/spec/issue_spec.rb +289 -0
  30. data/spec/jira_spec.rb +314 -0
  31. data/spec/json_resource_spec.rb +222 -0
  32. data/spec/mockapp/config.ru +6 -0
  33. data/spec/mockapp/index.html +10 -0
  34. data/spec/mockapp/jira.rb +61 -0
  35. data/spec/mockapp/views/auth/login_failed.erb +1 -0
  36. data/spec/mockapp/views/auth/login_success.erb +7 -0
  37. data/spec/mockapp/views/error.erb +3 -0
  38. data/spec/mockapp/views/field.erb +32 -0
  39. data/spec/mockapp/views/issue/TST-1.erb +186 -0
  40. data/spec/mockapp/views/issue/createmeta.erb +35 -0
  41. data/spec/mockapp/views/issue/err_nonexistent.erb +1 -0
  42. data/spec/mockapp/views/project/TST.erb +97 -0
  43. data/spec/mockapp/views/project/err_nonexistent.erb +4 -0
  44. data/spec/mockapp/views/search.erb +26 -0
  45. data/spec/project_spec.rb +20 -0
  46. data/spec/spec_helper.rb +26 -0
  47. data/tasks/mockjira.rake +10 -0
  48. data/tasks/pry.rake +28 -0
  49. data/tasks/spec.rake +9 -0
  50. data/tasks/test.rake +8 -0
  51. metadata +288 -0
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ jira/*
2
+ coverage/*
3
+ .rvmrc
4
+ .ruby-version
5
+ .ruby-gemset
6
+ Gemfile.lock
7
+ mockjira.pid
8
+ doc/*
9
+ .yardoc/*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ --pattern "spec/**/*_spec.rb"
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --readme README.md
2
+ --markup markdown
3
+ lib/**/*.rb
4
+ -
5
+ docs/index.md
6
+ docs/usage.md
7
+ docs/development.md
8
+ docs/history.md
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'bundler'
4
+
5
+ # Get all other dependencies from the gemspec
6
+ gemspec
7
+
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ Jiraby
2
+ ======
3
+
4
+ Jiraby is a Ruby wrapper for the [JIRA](http://www.atlassian.com/JIRA)
5
+ [REST API](https://docs.atlassian.com/jira/REST/latest/), supporting Jira
6
+ versions 6.2 and up.
7
+
8
+ [Full documentation is on rdoc.info](http://rubydoc.info/github/a-e/jiraby/master/frames).
9
+
10
+
11
+ Connect to Jira
12
+ ---------------
13
+
14
+ Assuming your JIRA site is at `http://jira.enterprise.com`, and you have
15
+ an account `picard` with password `earlgrey`, you can connect like so:
16
+
17
+ require 'jiraby'
18
+
19
+ host = 'jira.enterprise.com:8080' # :PORT is optional
20
+ username = 'picard'
21
+ password = 'earlgrey'
22
+
23
+ jira = Jiraby::Jira.new(host, username, password)
24
+
25
+ [HTTP basic](http://en.wikipedia.org/wiki/Basic_access_authentication)
26
+ authentication is used for all requests.
27
+
28
+
29
+ REST API
30
+ --------
31
+
32
+ Methods in the [JIRA REST API](https://docs.atlassian.com/jira/REST/6.2/) can be
33
+ accessed directly using the `#get`, `#put`, `#post`, and `#delete` methods:
34
+
35
+ jira.get 'serverInfo' # info about Jira server
36
+ jira.get 'issue/TEST-1' # full details of TEST-1 issue
37
+ jira.get 'field' # all fields, both System and Custom
38
+ jira.get 'user/search', :username => 'bob' # all users matching "bob"
39
+ jira.get 'user/search?username=bob' # all users matching "bob"
40
+
41
+ jira.put 'issue/TEST-1', :fields => { # set one or more fields
42
+ :summary => "Modified summary",
43
+ :description => "New description"
44
+ }
45
+
46
+ jira.delete 'issue/TEST-1' # delete issue TEST-1
47
+
48
+ All REST methods return a `Jiraby::Entity` (a hash-like object built directly from
49
+ the JSON response), or an `Array` of them (for those REST methods that return arrays).
50
+
51
+
52
+ Wrappers
53
+ --------
54
+
55
+ You can look up a Jira issue using the `#issue` method:
56
+
57
+ issue = jira.issue('myproj-15')
58
+ issue = jira.issue('MYPROJ-15') # case-insensitive
59
+
60
+ issue.class
61
+ # => Jiraby::Issue
62
+
63
+ If you're interested, view the raw data returned from Jira:
64
+
65
+ issue.data
66
+ # => {
67
+ # 'id' => '10024',
68
+ # 'key' => 'MYPROJ-15',
69
+ # 'self' => 'http://jira.enterprise.com:8080/rest/api/2/issue/10024',
70
+ # 'fields' => {
71
+ # 'summary' => 'Realign the dilithium stabilizer matrix.',
72
+ # ...
73
+ # }
74
+ # }
75
+
76
+ Or use the higher-level methods provided by the `Issue` class:
77
+
78
+ issue['foo'] # Value of field 'foo'; same as `issue.data.fields.foo`
79
+ issue['foo'] = "Newval" # Assign to field 'foo'
80
+ issue.subtasks # Array of issue keys for this issue's subtasks
81
+ issue.is_subtask? # True if issue is a sub-task of another issue
82
+ issue.parent # For subtasks, issue key of parent issue
83
+ issue.is_assigned? # True if issue is assigned
84
+
85
+ When modifying fields, the changes will appear in the `Issue` instance immediately:
86
+
87
+ issue['summary'] = "Modified summary"
88
+
89
+ issue['summary']
90
+ # => "Modified summary"
91
+
92
+ But these changes are not saved back to Jira until you call `#save!`. Before
93
+ saving, you can check for pending changes:
94
+
95
+ issue.pending_changes?
96
+ # => true
97
+
98
+ issue.pending_changes
99
+ # => {"summary" => "Modified summary"}
100
+
101
+ Then save the updates back to Jira:
102
+
103
+ issue.save!
104
+ # => true
105
+
106
+
107
+ Copyright
108
+ ---------
109
+
110
+ The MIT License
111
+
112
+ Copyright (c) 2014 Eric Pierce
113
+
114
+ Permission is hereby granted, free of charge, to any person obtaining
115
+ a copy of this software and associated documentation files (the
116
+ "Software"), to deal in the Software without restriction, including
117
+ without limitation the rights to use, copy, modify, merge, publish,
118
+ distribute, sublicense, and/or sell copies of the Software, and to
119
+ permit persons to whom the Software is furnished to do so, subject to
120
+ the following conditions:
121
+
122
+ The above copyright notice and this permission notice shall be
123
+ included in all copies or substantial portions of the Software.
124
+
125
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
126
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
127
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
128
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
129
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
130
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
131
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
132
+
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rake'
2
+ Dir.glob('tasks/*.rake').each { |r| import r }
3
+
4
+ task :default => [:test]
5
+
@@ -0,0 +1,20 @@
1
+ Development
2
+ ===========
3
+
4
+ Testing
5
+ -------
6
+
7
+ Jiraby is tested using a locally-installed instance of Jira. For licensing and
8
+ size reasons that should be obvious, this local instance is not included in the
9
+ Jiraby codebase itself. Here are the assumptions that the Jiraby test suite
10
+ makes about your local Jira instance:
11
+
12
+ - It's running at http://localhost:8080/
13
+ - There is an administrator (in the 'jira-administrators' group):
14
+ - Username: admin
15
+ - Password: admin
16
+ - There is a regular user (in the 'jira-users' group):
17
+ - Username: user
18
+ - Password: user
19
+ - There is a project called 'TST'
20
+
data/docs/history.md ADDED
@@ -0,0 +1,5 @@
1
+ Jiraby History
2
+ ==============
3
+
4
+ TODO
5
+
data/docs/ideas.md ADDED
@@ -0,0 +1,54 @@
1
+ Jiraby API ideas
2
+ ================
3
+
4
+ - Implement an attribute wrapper for hashes, to allow stuff like:
5
+ issue.project.key
6
+ issue.project.owner
7
+ (See http://pullmonkey.com/2008/01/06/convert-a-ruby-hash-into-a-class-object/)
8
+
9
+ Issue fields have different types. For example:
10
+
11
+ > iss.json['fields']['comment']
12
+ => {"comments"=>[], "startAt"=>0, "total"=>0, "maxResults"=>0}
13
+
14
+ > iss.json['fields']['environment']
15
+ => nil
16
+
17
+ > iss.json['fields']['summary']
18
+ => "It's broken"
19
+
20
+ > iss.json['fields']['subtasks']
21
+ => []
22
+
23
+ Further, these are different in different versions of Jira. The above are from
24
+ Jira 5.0, while Jira 4.4 produces:
25
+
26
+ > iss.json['fields']['comment']
27
+ => {"name"=>"comment", "value"=>[], "type"=>"com.atlassian.jira.issue.fields.CommentSystemField"}
28
+
29
+ > iss.json['fields']['environment']
30
+ => {"name"=>"environment", "type"=>"java.lang.String"}
31
+
32
+ > iss.json['fields']['summary']
33
+ => {"name"=>"summary", "value"=>"Cherwell incident #33170", "type"=>"java.lang.String"}
34
+
35
+ > iss.json['fields']['sub-tasks'] # NOTE: Different key
36
+ => {"name"=>"sub-tasks", "value"=>[], "type"=>"issuelinks"}
37
+
38
+ Maintaining compatibility between both of these could be a royal pain. It's
39
+ probably best to separate the REST / JSON stuff from the specific Jira/API
40
+ version we're using.
41
+
42
+ Consistency
43
+ -----------
44
+
45
+ The JIRA REST API is consistent in some things:
46
+
47
+ - It (almost) always returns JSON
48
+ Note: At least one method (PUT /issue/<key>) returns an empty string on success
49
+ - Use of `self` for each entity
50
+
51
+ Challenges:
52
+
53
+ - How to remain authenticated (pass JSONResource around to each REST request)?
54
+
data/docs/index.md ADDED
@@ -0,0 +1,11 @@
1
+ Jiraby
2
+ ======
3
+
4
+ Jiraby is a Ruby interface to the [JIRA](http://www.atlassian.com/JIRA)
5
+ [REST API](http://docs.atlassian.com/jira/REST/latest/).
6
+
7
+ - [Usage](usage.md)
8
+ - [Development](development.md)
9
+ - [Ideas](ideas.md)
10
+ - [History](history.md)
11
+
data/docs/usage.md ADDED
@@ -0,0 +1,64 @@
1
+ Usage
2
+ =====
3
+
4
+ Assuming your JIRA site is at `http://jira.enterprise.com`, and you have
5
+ an account `picard` with password `earlgrey`, you can connect like so:
6
+
7
+ require 'jiraby'
8
+
9
+ jira = Jiraby::Jira.new('http://jira.enterprise.com')
10
+ jira.login('picard', 'earlgrey')
11
+
12
+ Using Jiraby from the Ruby console:
13
+
14
+ $ bundle console
15
+ > require 'lib/jiraby/jira'
16
+ > jira = Jiraby::Jira.new('http://localhost:8080')
17
+ > jira.login('username', 'password')
18
+ > jira.logout
19
+
20
+ Methods in the [JIRA REST API](https://docs.atlassian.com/jira/REST/6.2/) can be
21
+ accessed like so:
22
+
23
+ > jira.get('issue/TEST-1')
24
+ => {"id"=>"10000",
25
+ "self"=>"http://localhost:8080/rest/api/2/issue/10000",
26
+ "key"=>"TEST-1",
27
+ "fields"=>{ ... }
28
+ }
29
+
30
+ > jira.get('serverInfo')
31
+ => {"baseUrl"=>"http://localhost:8080",
32
+ "version"=>"6.2",
33
+ "versionNumbers"=>[6, 2, 0],
34
+ "buildNumber"=>6252,
35
+ "buildDate"=>"2014-02-19T00:00:00.000-0700",
36
+ "serverTime"=>"2014-03-06T08:27:04.116-0700",
37
+ "scmInfo"=>"aa343257d4ce030d9cb8c531be520be9fac1c996",
38
+ "serverTitle"=>"Jiraby Test"}
39
+
40
+ > jira.get('resolution/1')
41
+ => {"self"=>"http://localhost:8080/rest/api/2/resolution/1",
42
+ "id"=>"1",
43
+ "description"=>"A fix for this issue is checked into the tree and tested.",
44
+ "name"=>"Fixed"}
45
+
46
+ Passing parameters to GET:
47
+
48
+ > jira.get('user/search?username=admin')
49
+ => [{"self"=>"http://localhost:8080/rest/api/2/user?username=admin",
50
+ "key"=>"admin",
51
+ "name"=>"admin",
52
+ "emailAddress"=>"epierce@automation-excellence.com",
53
+ "avatarUrls"=>
54
+ {"16x16"=>
55
+ "http://localhost:8080/secure/useravatar?size=xsmall&avatarId=10122",
56
+ "24x24"=>
57
+ "http://localhost:8080/secure/useravatar?size=small&avatarId=10122",
58
+ "32x32"=>
59
+ "http://localhost:8080/secure/useravatar?size=medium&avatarId=10122",
60
+ "48x48"=>"http://localhost:8080/secure/useravatar?avatarId=10122"},
61
+ "displayName"=>"Admin Istrator",
62
+ "active"=>true,
63
+ "timeZone"=>"America/Denver"}]
64
+
data/jiraby.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "jiraby"
3
+ s.version = "0.0.1"
4
+ s.summary = "Jira-Ruby bridge"
5
+ s.description = <<-EOS
6
+ Jiraby is a Ruby wrapper for the JIRA REST API,
7
+ supporting Jira 6.x.
8
+ EOS
9
+ s.authors = ["Eric Pierce"]
10
+ s.email = "wapcaplet88@gmail.com"
11
+ s.homepage = "http://github.com/a-e/jiraby"
12
+ s.platform = Gem::Platform::RUBY
13
+
14
+ s.add_dependency 'rest-client'
15
+ s.add_dependency 'yajl-ruby'
16
+ s.add_dependency 'hashie'
17
+
18
+ s.add_development_dependency 'rake'
19
+ s.add_development_dependency 'rspec'
20
+ s.add_development_dependency 'simplecov'
21
+ s.add_development_dependency 'pry'
22
+ s.add_development_dependency 'sinatra'
23
+ s.add_development_dependency 'rakeup'
24
+ s.add_development_dependency 'thin'
25
+ s.add_development_dependency 'yard'
26
+ s.add_development_dependency 'redcarpet'
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.require_path = 'lib'
30
+ end
31
+
data/lib/jiraby.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'jiraby/exceptions'
2
+ require 'jiraby/issue'
3
+ require 'jiraby/jira'
4
+ require 'jiraby/json_resource'
5
+ require 'jiraby/project'
6
+
7
+ module Jiraby
8
+ end
@@ -0,0 +1,21 @@
1
+ require 'hashie'
2
+
3
+ module Jiraby
4
+ # Represents some data structure in Jira, as it would be returned by
5
+ # a REST API method.
6
+ class Entity < Hashie::Mash
7
+ # If no `args` are given, return the value in the `key` field (often used
8
+ # as an identifier in Jira). If `args` are given, pass-through to Hash's
9
+ # regular `key` method (returning the key for a given value).
10
+ def key(*args)
11
+ # Pass-through to Hash's `key` method
12
+ if args.length > 0
13
+ super(*args)
14
+ else
15
+ return self['key']
16
+ end
17
+ end
18
+
19
+ end # class Entity
20
+ end # module Jiraby
21
+
@@ -0,0 +1,8 @@
1
+ module Jiraby
2
+ class JirabyError < RuntimeError; end
3
+ class ProjectNotFound < JirabyError; end
4
+ class IssueNotFound < JirabyError; end
5
+ class RestCallFailed < JirabyError; end
6
+ class InvalidField < JirabyError; end
7
+ end # module Jiraby
8
+
@@ -0,0 +1,109 @@
1
+ require 'jiraby/exceptions'
2
+ require 'jiraby/entity'
3
+
4
+ module Jiraby
5
+ class Issue
6
+ def initialize(jira_instance, json_data={})
7
+ @jira = jira_instance
8
+ @data = Entity.new(json_data)
9
+ # Modifications are stored here until #save is called
10
+ @pending_changes = Entity.new
11
+ end
12
+
13
+ attr_reader :jira, :data, :pending_changes
14
+
15
+ # Return this issue's `key`
16
+ def key
17
+ return @data.key
18
+ end
19
+
20
+ # Set field `name_or_id` equal to `value`.
21
+ def []=(name_or_id, value)
22
+ @pending_changes[self.field_id(name_or_id)] = value
23
+ end
24
+
25
+ # Return the value in field `name_or_id`.
26
+ def [](name_or_id)
27
+ _id = self.field_id(name_or_id)
28
+ return @pending_changes[_id] || @data.fields[_id]
29
+ end
30
+
31
+ # Return a field ID, given a name or ID. `name_or_id` may be the field's ID
32
+ # (like "subtasks" or "customfield_10001"), or it may be the human-readable
33
+ # field name (like "Sub-Tasks" or "My Custom Field").
34
+ #
35
+ # TODO: Raise an exception on invalid name_or_id?
36
+ def field_id(name_or_id)
37
+ if @data.fields.include?(name_or_id)
38
+ return name_or_id
39
+ else
40
+ _id = @jira.field_mapping.key(name_or_id)
41
+ if _id.nil?
42
+ raise InvalidField.new("Invalid field name or ID: #{name_or_id}")
43
+ end
44
+ return _id
45
+ end
46
+ end
47
+
48
+ # Return true if this issue has a field with the given name or ID,
49
+ # false otherwise.
50
+ def has_field?(name_or_id)
51
+ begin
52
+ self.field_id(name_or_id)
53
+ rescue InvalidField
54
+ return false
55
+ else
56
+ return true
57
+ end
58
+ end
59
+
60
+ # Return true if this issue is a subtask, false otherwise.
61
+ def is_subtask?
62
+ return @data.fields.issuetype.subtask
63
+ end
64
+
65
+ # Return true if this issue is assigned to someone, false otherwise.
66
+ def is_assigned?
67
+ return !@data.fields.assignee.nil?
68
+ end
69
+
70
+ # Return this issue's parent key, or nil if this issue has no parent.
71
+ def parent
72
+ if is_subtask?
73
+ return @data.fields.parent.key
74
+ else
75
+ return nil
76
+ end
77
+ end
78
+
79
+ # Return this issue's subtask keys
80
+ def subtasks
81
+ return @data.fields.subtasks.collect { |st| st.key }
82
+ end
83
+
84
+ # Return a sorted list of valid field IDs for this issue.
85
+ def field_ids
86
+ return @data.fields.keys.sort
87
+ end
88
+
89
+ def editmeta
90
+ return @jira.get("issue/#{@data.key}/editmeta")
91
+ end
92
+
93
+ # Return true if this issue has been modified since saving.
94
+ def pending_changes?
95
+ return !@pending_changes.empty?
96
+ end
97
+
98
+ # Save this issue by sending a PUT request.
99
+ # Return true if save was successful.
100
+ def save!
101
+ json_data = {'fields' => @pending_changes}
102
+ # TODO: Handle failed save
103
+ @jira.put("issue/#{@data.key}", json_data)
104
+ @data.fields.merge!(@pending_changes)
105
+ @pending_changes = Entity.new
106
+ return true
107
+ end
108
+ end
109
+ end