nofxx-pickler 0.0.10

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.
@@ -0,0 +1,116 @@
1
+ require 'date'
2
+ require 'cgi'
3
+
4
+ class Pickler
5
+ class Tracker
6
+
7
+ ADDRESS = 'www.pivotaltracker.com'
8
+ BASE_PATH = '/services/v2'
9
+ SEARCH_KEYS = %w(label type state requester owner mywork id includedone)
10
+
11
+ class Error < Pickler::Error; end
12
+
13
+ attr_reader :token
14
+
15
+ def initialize(token, ssl = false)
16
+ require 'active_support'
17
+ @token = token
18
+ @ssl = ssl
19
+ end
20
+
21
+ def ssl?
22
+ @ssl
23
+ end
24
+
25
+ def http
26
+ unless @http
27
+ if ssl?
28
+ require 'net/https'
29
+ @http = Net::HTTP.new(ADDRESS, Net::HTTP.https_default_port)
30
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
31
+ @http.use_ssl = true
32
+ else
33
+ require 'net/http'
34
+ @http = Net::HTTP.new(ADDRESS)
35
+ end
36
+ end
37
+ @http
38
+ end
39
+
40
+ def request(method, path, *args)
41
+ headers = {
42
+ "X-TrackerToken" => @token,
43
+ "Accept" => "application/xml",
44
+ "Content-type" => "application/xml"
45
+ }
46
+ http # trigger require of 'net/http'
47
+ klass = Net::HTTP.const_get(method.to_s.capitalize)
48
+ http.request(klass.new("#{BASE_PATH}#{path}", headers), *args)
49
+ end
50
+
51
+ def request_xml(method, path, *args)
52
+ response = request(method,path,*args)
53
+ raise response.inspect if response["Content-type"].split(/; */).first != "application/xml"
54
+ hash = Hash.from_xml(response.body)
55
+ if hash["message"] && (response.code.to_i >= 400 || hash["success"] == "false")
56
+ raise Error, hash["message"], caller
57
+ end
58
+ hash
59
+ end
60
+
61
+ def get_xml(path)
62
+ request_xml(:get, path)
63
+ end
64
+
65
+ def project(id)
66
+ Project.new(self,get_xml("/projects/#{id}")["project"])
67
+ end
68
+
69
+ class Abstract
70
+ def initialize(attributes = {})
71
+ @attributes = {}
72
+ (attributes || {}).each do |k,v|
73
+ @attributes[k.to_s] = v
74
+ end
75
+ yield self if block_given?
76
+ end
77
+
78
+ def self.reader(*methods)
79
+ methods.each do |method|
80
+ define_method(method) { @attributes[method.to_s] }
81
+ end
82
+ end
83
+
84
+ def self.date_reader(*methods)
85
+ methods.each do |method|
86
+ define_method(method) do
87
+ value = @attributes[method.to_s]
88
+ value.kind_of?(String) ? Date.parse(value) : value
89
+ end
90
+ end
91
+ end
92
+
93
+ def self.accessor(*methods)
94
+ reader(*methods)
95
+ methods.each do |method|
96
+ define_method("#{method}=") { |v| @attributes[method.to_s] = v }
97
+ end
98
+ end
99
+
100
+ def id
101
+ id = @attributes['id'] and Integer(id)
102
+ end
103
+
104
+ def to_xml(options = nil)
105
+ @attributes.to_xml({:dasherize => false, :root => self.class.name.split('::').last.downcase}.merge(options||{}))
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+ end
112
+
113
+ require 'pickler/tracker/project'
114
+ require 'pickler/tracker/story'
115
+ require 'pickler/tracker/iteration'
116
+ require 'pickler/tracker/note'
@@ -0,0 +1,45 @@
1
+ class Pickler
2
+ class Tracker
3
+ class Iteration < Abstract
4
+ attr_reader :project
5
+
6
+ def initialize(project, attributes = {})
7
+ @project = project
8
+ super(attributes)
9
+ end
10
+
11
+ def start
12
+ Date.parse(@attributes['start'].to_s)
13
+ end
14
+
15
+ def finish
16
+ Date.parse(@attributes['finish'].to_s)
17
+ end
18
+
19
+ def number
20
+ @attributes['number'].to_i
21
+ end
22
+ alias to_i number
23
+
24
+ def range
25
+ start...finish
26
+ end
27
+
28
+ def include?(date)
29
+ range.include?(date)
30
+ end
31
+
32
+ def succ
33
+ self.class.new(project, 'number' => number.succ.to_s, 'start' => @attributes['finish'], 'finish' => (finish + (finish - start)))
34
+ end
35
+
36
+ def inspect
37
+ "#<#{self.class.inspect}:#{number.inspect} (#{range.inspect})>"
38
+ end
39
+
40
+ def to_s
41
+ "#{number} (#{start}...#{finish})"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ class Pickler
2
+ class Tracker
3
+ class Note < Abstract
4
+ attr_reader :story
5
+ reader :text, :author
6
+ date_reader :noted_at
7
+
8
+ def date
9
+ noted_at && Date.new(noted_at.year, noted_at.mon, noted_at.day)
10
+ end
11
+
12
+ def initialize(story, attributes = {})
13
+ @story = story
14
+ super(attributes)
15
+ end
16
+
17
+ def to_xml
18
+ @attributes.to_xml(:dasherize => false, :root => 'note')
19
+ end
20
+
21
+ def inspect
22
+ "#<#{self.class.inspect}:#{id.inspect}, story_id: #{story.id.inspect}, date: #{date.inspect}, author: #{author.inspect}, text: #{text.inspect}>"
23
+ end
24
+
25
+ def lines(width = 79)
26
+ text.scan(/(?:.{0,#{width}}|\S+?)(?:\s|$)/).map! {|line| line.strip}[0..-2]
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ class Pickler
2
+ class Tracker
3
+ class Project < Abstract
4
+
5
+ attr_reader :tracker
6
+ reader :point_scale, :week_start_day, :name, :iteration_length
7
+
8
+ def initialize(tracker, attributes = {})
9
+ @tracker = tracker
10
+ super(attributes)
11
+ end
12
+
13
+ def story(story_id)
14
+ raise Error, "No story id given" if story_id.to_s.empty?
15
+ Story.new(self,tracker.get_xml("/projects/#{id}/stories/#{story_id}")["story"])
16
+ end
17
+
18
+ def stories(*args)
19
+ filter = encode_term(args) if args.any?
20
+ path = "/projects/#{id}/stories"
21
+ path << "?filter=#{CGI.escape(filter)}" if filter
22
+ response = tracker.get_xml(path)
23
+ [response["stories"]].flatten.compact.map {|s| Story.new(self,s)}
24
+ end
25
+
26
+ def new_story(attributes = {}, &block)
27
+ Story.new(self, attributes, &block)
28
+ end
29
+
30
+ def deliver_all_finished_stories
31
+ request_xml(:put,"/projects/#{id}/stories_deliver_all_finished")
32
+ end
33
+
34
+ private
35
+ def encode_term(term)
36
+ case term
37
+ when Array then term.map {|v| encode_term(v)}.join(" ")
38
+ when Hash then term.map {|k,v| encode_term("#{k}:#{v}")}.join(" ")
39
+ when /^\S+$/, Symbol then term
40
+ when /^(\S+?):(.*)$/ then %{#$1:"#$2"}
41
+ else %{"#{term}"}
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,193 @@
1
+ class Pickler
2
+ class Tracker
3
+ class Story < Abstract
4
+
5
+ TYPES = %w(bug feature chore release)
6
+ STATES = %w(unscheduled unstarted started finished delivered rejected accepted)
7
+
8
+ attr_reader :project, :labels
9
+ reader :url
10
+ date_reader :created_at, :accepted_at, :deadline
11
+ accessor :current_state, :name, :description, :owned_by, :requested_by, :story_type
12
+
13
+ def initialize(project, attributes = {})
14
+ @project = project
15
+ super(attributes)
16
+ @iteration = Iteration.new(project, @attributes["iteration"]) if @attributes["iteration"]
17
+ @labels = normalize_labels(@attributes["labels"])
18
+ end
19
+
20
+ def iteration
21
+ unless current_state == 'unscheduled' || defined?(@iteration)
22
+ @iteration = project.stories(:id => id, :includedone => true).first.iteration
23
+ end
24
+ @iteration
25
+ end
26
+
27
+ def labels=(value)
28
+ @labels = normalize_labels(value)
29
+ end
30
+
31
+ def transition!(state)
32
+ raise Pickler::Tracker::Error, "Invalid state #{state}", caller unless STATES.include?(state)
33
+ self.current_state = state
34
+ if id
35
+ xml = "<story><current_state>#{state}</current_state></story>"
36
+ error = tracker.request_xml(:put, resource_url, xml).fetch("errors",{})["error"] || true
37
+ else
38
+ error = save
39
+ end
40
+ raise Pickler::Tracker::Error, Array(error).join("\n"), caller unless error == true
41
+ end
42
+
43
+ def finish
44
+ case story_type
45
+ when "bug", "feature"
46
+ self.current_state = "finished" unless complete?
47
+ when "chore", "release"
48
+ self.current_state = "accepted"
49
+ end
50
+ current_state
51
+ end
52
+
53
+ def finish!
54
+ transition!(finish)
55
+ end
56
+
57
+ def backlog?(as_of = Date.today)
58
+ iteration && iteration.start >= as_of
59
+ end
60
+
61
+ def current?(as_of = Date.today)
62
+ iteration && iteration.include?(as_of)
63
+ end
64
+
65
+ # In a previous iteration
66
+ def done?(as_of = Date.today)
67
+ iteration && iteration.finish <= as_of
68
+ end
69
+
70
+ def complete?
71
+ %w(finished delivered accepted).include?(current_state)
72
+ end
73
+
74
+ def startable?
75
+ %w(unscheduled unstarted rejected).include?(current_state)
76
+ end
77
+
78
+ def tracker
79
+ project.tracker
80
+ end
81
+
82
+ def to_s(format = :comment)
83
+ to_s = "#{header(format)}\n#{story_type.capitalize}: #{name}\n"
84
+ description_lines.each do |line|
85
+ to_s << " #{line}".rstrip << "\n"
86
+ end
87
+ to_s
88
+ end
89
+
90
+ def header(format = :comment)
91
+ case format
92
+ when :tag
93
+ "@#{url}#{labels.map {|l| " @#{l.tr('_,',' _')}"}.join}"
94
+ else
95
+ "# #{url}"
96
+ end
97
+ end
98
+
99
+ def to_s=(body)
100
+ if body =~ /\A@https?\b\S*(\s+@\S+)*\s*$/
101
+ self.labels = body[/\A@.*/].split(/\s+/)[1..-1].map {|l| l[1..-1].tr(' _','_,')}
102
+ end
103
+ body = body.sub(/\A(?:[@#].*\n)+/,'')
104
+ if body =~ /\A(\w+): (.*)/
105
+ self.story_type = $1.downcase
106
+ self.name = $2
107
+ description = $'
108
+ else
109
+ self.story_type = "feature"
110
+ self.name = body[/.*/]
111
+ description = $'
112
+ end
113
+ self.description = description.gsub(/\A\n+|\n+\Z/,'') + "\n"
114
+ if description_lines.all? {|l| l.empty? || l =~ /^ /}
115
+ self.description.gsub!(/^ /,'')
116
+ end
117
+ self
118
+ end
119
+
120
+ def description_lines
121
+ array = []
122
+ description.to_s.each_line do |line|
123
+ array << line.chomp
124
+ end
125
+ array
126
+ end
127
+
128
+ def notes
129
+ [@attributes["notes"]].flatten.compact.map {|n| Note.new(self,n)}
130
+ end
131
+
132
+ def estimate
133
+ @attributes["estimate"].to_i < 0 ? nil : @attributes["estimate"]
134
+ end
135
+
136
+ def comment!(body)
137
+ response = tracker.request_xml(:post, "#{resource_url}/notes",{:text => body}.to_xml(:dasherize => false, :root => 'note'))
138
+ if response["note"]
139
+ Note.new(self, response["note"])
140
+ else
141
+ raise Pickler::Tracker::Error, Array(response["errors"]["error"]).join("\n"), caller
142
+ end
143
+ end
144
+
145
+ def to_xml(force_labels = true)
146
+ hash = @attributes.reject do |k,v|
147
+ !%w(current_state deadline description estimate name owned_by requested_by story_type).include?(k)
148
+ end
149
+ if force_labels || !id || normalize_labels(@attributes["labels"]) != labels
150
+ hash["labels"] = labels.join(", ")
151
+ end
152
+ hash.to_xml(:dasherize => false, :root => "story")
153
+ end
154
+
155
+ def destroy
156
+ if id
157
+ response = tracker.request_xml(:delete, "/projects/#{project.id}/stories/#{id}", "")
158
+ raise Error, response["message"], caller if response["message"]
159
+ @attributes["id"] = nil
160
+ self
161
+ end
162
+ end
163
+
164
+ def resource_url
165
+ ["/projects/#{project.id}/stories",id].compact.join("/")
166
+ end
167
+
168
+ def save
169
+ response = tracker.request_xml(id ? :put : :post, resource_url, to_xml(false))
170
+ if response["story"]
171
+ initialize(project, response["story"])
172
+ true
173
+ else
174
+ Array(response["errors"]["error"])
175
+ end
176
+ end
177
+
178
+ def save!
179
+ errors = save
180
+ if errors != true
181
+ raise Pickler::Tracker::Error, Array(errors).join("\n"), caller
182
+ end
183
+ self
184
+ end
185
+
186
+ private
187
+ def normalize_labels(value)
188
+ Array(value).join(", ").strip.split(/\s*,\s*/)
189
+ end
190
+
191
+ end
192
+ end
193
+ end
data/pickler.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "pickler"
3
+ s.version = "0.0.10"
4
+
5
+ s.summary = "PIvotal traCKer Liaison to cucumbER"
6
+ s.description = "Synchronize between Cucumber and Pivotal Tracker"
7
+ s.authors = ["Tim Pope"]
8
+ s.email = "ruby@tpope.i"+'nfo'
9
+ s.homepage = "http://github.com/tpope/pickler"
10
+ s.default_executable = "pickler"
11
+ s.executables = ["piv", "pickler"]
12
+ s.files = [
13
+ "README.rdoc",
14
+ "MIT-LICENSE",
15
+ "pickler.gemspec",
16
+ "bin/piv",
17
+ "bin/pickler",
18
+ "lib/pickler.rb",
19
+ "lib/pickler/feature.rb",
20
+ "lib/pickler/runner.rb",
21
+ "lib/pickler/tracker.rb",
22
+ "lib/pickler/tracker/project.rb",
23
+ "lib/pickler/tracker/story.rb",
24
+ "lib/pickler/tracker/iteration.rb",
25
+ "lib/pickler/tracker/note.rb"
26
+ ]
27
+ s.add_dependency("activesupport", [">= 2.0.0"])
28
+ s.add_dependency("cucumber", [">= 0.3.0"])
29
+ end