slurper 0.1.0

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.
data/README.rdoc ADDED
@@ -0,0 +1,58 @@
1
+ = slurper
2
+
3
+ Slurper allows you to quickly compose your stories in a text file and import them into Pivotal Tracker.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Setup
16
+
17
+ gem install hashrocket-slurper
18
+
19
+ == Config
20
+
21
+ Create a story_defaults.yml file in your working directory for project stories.
22
+
23
+ === Example
24
+
25
+ project_id: 1234
26
+ token: 123abc123abc123abc
27
+ name: Untitled
28
+ description: "In order to \nAs a \nI want \n\nAcceptance:\n* "
29
+ requested_by: Jane Stakeholder
30
+ labels: slurper
31
+
32
+ The project_id tells tracker which project to add your stories to. It can be found on the project settings or the url for the project.
33
+
34
+ The token can be found on your personal profile page in Pivotal Tracker.
35
+
36
+ The name, labels and description fields provide slurper with default values for story titles and descriptoins if you don't provide them in your stories.txt file.
37
+
38
+ The requested_by field should be the name of your project stakeholder exactly as it appears in tracker.
39
+
40
+ == Usage
41
+
42
+ Create a stories.txt file and compose your stories in the slurper story format.
43
+
44
+ === Example
45
+
46
+ ==
47
+ name
48
+ User Create Something
49
+ description
50
+ In order to accomplish some business value
51
+ As a User
52
+ I want to create something
53
+
54
+ labels
55
+ things
56
+ ==
57
+
58
+ Note: stories must have == before and after each story.
data/bin/slurp ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Slurps stories from the given file (stories.txt by default) and creates
4
+ # Pivotal Tracker stories from them. Useful during story carding sessions
5
+ # when you want to capture a number of stories quickly without clicking
6
+ # your way through the Tracker UI.
7
+
8
+ # Note that if you include labels in stories.txt, they don't appear
9
+ # immediately in Tracker. You'll have to refresh Tracker after a few seconds
10
+ # to see them.
11
+
12
+ $:.unshift(File.join(File.dirname(File.dirname(__FILE__)),'lib'))
13
+ require 'rubygems'
14
+ require 'slurper'
15
+ require 'optparse'
16
+
17
+ options = {}
18
+ OptionParser.new do |opts|
19
+ opts.on("-r", "--reverse", "Reverse story creation order") do |v|
20
+ options[:reverse] = v
21
+ end
22
+ end.parse!
23
+
24
+ story_file = ARGV.empty? ? "stories.txt" : ARGV[0]
25
+
26
+ story_lines = Array.new
27
+ stories = Array.new
28
+ IO.foreach(story_file) do |line|
29
+ if line[0,2] != "=="
30
+ story_lines << line
31
+ else
32
+ stories << Story.new.parse(story_lines)
33
+ story_lines.clear
34
+ end
35
+ end
36
+
37
+ stories.reverse! unless options[:reverse]
38
+
39
+ stories.each { |story| story.save }
data/lib/slurper.rb ADDED
@@ -0,0 +1 @@
1
+ require 'story'
data/lib/story.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'activeresource'
2
+
3
+ class Story < ActiveResource::Base
4
+
5
+ @@defaults = YAML.load_file('story_defaults.yml')
6
+ self.site = "http://www.pivotaltracker.com/services/v2/projects/#{@@defaults['project_id']}"
7
+ headers['X-TrackerToken'] = @@defaults.delete("token")
8
+ attr_accessor :story_lines
9
+
10
+ def initialize(attributes = {})
11
+ @attributes = {}
12
+ @prefix_options = {}
13
+ load(@@defaults.merge(attributes))
14
+ end
15
+
16
+ def parse(story_lines)
17
+ @story_lines = story_lines
18
+ parse_name
19
+ parse_description
20
+ parse_labels
21
+ self
22
+ end
23
+
24
+ private
25
+
26
+ def parse_name
27
+ @story_lines.each_with_index do |line, i|
28
+ if start_of_value?(line, 'name')
29
+ if starts_with_whitespace?(@story_lines[i+1])
30
+ @attributes["name"] = @story_lines[i+1].strip
31
+ else
32
+ @attributes.delete("name")
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def parse_description
39
+ @story_lines.each_with_index do |line, i|
40
+ if start_of_value?(line, 'description')
41
+ desc = Array.new
42
+ while((next_line = @story_lines[i+=1]) && starts_with_whitespace?(next_line)) do
43
+ desc << next_line
44
+ end
45
+ desc.empty? ? @attributes.delete("description") : @attributes["description"] = desc.join.gsub(/^ +/, "").gsub(/^\t+/, "")
46
+ end
47
+ end
48
+ end
49
+
50
+ def parse_labels
51
+ @story_lines.each_with_index do |line, i|
52
+ if start_of_value?(line, 'labels')
53
+ if starts_with_whitespace?(@story_lines[i+1])
54
+ @attributes["labels"] = @story_lines[i+1].strip
55
+ else
56
+ @attributes.delete("labels")
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def starts_with_whitespace?(line)
63
+ line && line[0,1] =~ /\s/
64
+ end
65
+
66
+ def start_of_value?(line, attribute)
67
+ line[0,attribute.size] == attribute
68
+ end
69
+
70
+ end
@@ -0,0 +1,95 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'story'
4
+
5
+ describe Story do
6
+
7
+ it ".parse should return a reference to the story" do
8
+ story = Story.new
9
+ story.parse("").should == story
10
+ end
11
+
12
+ context "deals with leading/trailing whitespace" do
13
+ before do
14
+ story_lines = IO.readlines(File.dirname(__FILE__) + "/whitespacey_story.txt")
15
+ @story = Story.new.parse(story_lines)
16
+ end
17
+
18
+ it "strips whitespace from the name" do
19
+ @story.name.should == "Profit"
20
+ end
21
+
22
+ it "strips whitespace from the description" do
23
+ @story.description.should == "In order to do something\nAs a role\nI want to click a thingy\n"
24
+ end
25
+ end
26
+
27
+ context "given values for all attributes" do
28
+ before do
29
+ story_lines = IO.readlines(File.dirname(__FILE__) + "/full_story.txt")
30
+ @story = Story.new.parse(story_lines)
31
+ end
32
+
33
+ it "parses the name correctly" do
34
+ @story.name.should == "Profit"
35
+ end
36
+
37
+ it "parses the description correctly" do
38
+ @story.description.should == "In order to do something\nAs a role\nI want to click a thingy\n\nAcceptance:\n* do the thing\n* don't forget the other thing\n"
39
+ end
40
+
41
+ it "parses the label correctly" do
42
+ @story.labels.should == "money,power,fame"
43
+ end
44
+ end
45
+
46
+ context "given only a name" do
47
+ before do
48
+ story_lines = IO.readlines(File.dirname(__FILE__) + "/name_only.txt")
49
+ @story = Story.new.parse(story_lines)
50
+ end
51
+
52
+ it "should parse the name correctly" do
53
+ @story.name.should == "Profit"
54
+ end
55
+
56
+ it "should use the default value for the description" do
57
+ @story.description.should == "In order to \nAs a \nI want \n\nAcceptance:\n* "
58
+ end
59
+
60
+ it "should use the default value for the labels" do
61
+ @story.labels.should == "slurper"
62
+ end
63
+ end
64
+
65
+ context "given empty attributes" do
66
+ before do
67
+ story_lines = IO.readlines(File.dirname(__FILE__) + "/empty_attributes.txt")
68
+ @story = Story.new.parse(story_lines)
69
+ end
70
+
71
+ it "should not set any name" do
72
+ @story.attributes.keys.should_not include("name")
73
+ end
74
+
75
+ it "should not set any description" do
76
+ @story.attributes.keys.should_not include("description")
77
+ end
78
+
79
+ it "should not set any labels" do
80
+ @story.attributes.keys.should_not include("labels")
81
+ end
82
+ end
83
+
84
+ context "given attributes with spaces" do
85
+ before do
86
+ story_lines = IO.readlines(File.dirname(__FILE__) + "/quoted_attributes.txt")
87
+ @story = Story.new.parse(story_lines)
88
+ end
89
+
90
+ it "should set the description correctly" do
91
+ @story.description.should == "I have a \"quote\"\n"
92
+ end
93
+ end
94
+
95
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slurper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wes Gibbs
8
+ - Adam Lowe
9
+ - Stephen Caudill
10
+ - Tim Pope
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2009-12-16 00:00:00 -05:00
16
+ default_executable: slurp
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: rspec
20
+ type: :development
21
+ version_requirement:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.9
27
+ version:
28
+ description: "\n Slurps stories from the given file (stories.txt by default) and creates\n Pivotal Tracker stories from them. Useful during story carding sessions\n when you want to capture a number of stories quickly without clicking\n your way through the Tracker UI.\n "
29
+ email: info@hashrocket.com
30
+ executables:
31
+ - slurp
32
+ extensions: []
33
+
34
+ extra_rdoc_files:
35
+ - README.rdoc
36
+ files:
37
+ - bin/slurp
38
+ - lib/slurper.rb
39
+ - lib/story.rb
40
+ - README.rdoc
41
+ has_rdoc: true
42
+ homepage: http://github.com/hashrocket/slurper
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.3.5
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: takes a formatted story file and puts it on Pivotal Tracker
69
+ test_files:
70
+ - spec/story_spec.rb