saga 0.7.1 → 0.8.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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.1
1
+ 0.8.0
data/lib/saga/document.rb CHANGED
@@ -12,10 +12,22 @@ module Saga
12
12
  @definitions = ActiveSupport::OrderedHash.new
13
13
  end
14
14
 
15
+ def stories_as_flat_list
16
+ stories_as_flat_list = []
17
+ stories.values.flatten.each do |story|
18
+ stories_as_flat_list << story
19
+ stories_as_flat_list.concat(story[:stories]) if story[:stories]
20
+ end; stories_as_flat_list
21
+ end
22
+
15
23
  def used_ids
16
24
  @stories.values.inject([]) do |ids, stories|
17
- ids.concat stories.map { |story| story[:id] }
18
- ids
25
+ stories.each do |story|
26
+ ids << story[:id]
27
+ story[:stories].each do |nested|
28
+ ids << nested[:id]
29
+ end if story[:stories]
30
+ end; ids
19
31
  end.compact
20
32
  end
21
33
 
@@ -30,7 +42,7 @@ module Saga
30
42
  end
31
43
 
32
44
  def length
33
- stories.inject(0) { |total, (_, stories)| total + stories.length }
45
+ stories_as_flat_list.length
34
46
  end
35
47
 
36
48
  def empty?
@@ -39,10 +51,8 @@ module Saga
39
51
 
40
52
  def autofill_ids
41
53
  unused_ids = unused_ids(length - used_ids.length)
42
- stories.each do |_, stories|
43
- stories.each do |story|
44
- story[:id] ||= unused_ids.shift
45
- end
54
+ stories_as_flat_list.each do |story|
55
+ story[:id] ||= unused_ids.shift
46
56
  end
47
57
  end
48
58
  end
data/lib/saga/parser.rb CHANGED
@@ -24,6 +24,22 @@ module Saga
24
24
  @document.stories[@current_header] << story
25
25
  end
26
26
 
27
+ def handle_nested_story(story)
28
+ @current_section = :story
29
+ parent = @document.stories[@current_header][-1]
30
+ parent[:stories] ||= []
31
+ parent[:stories] << story
32
+ end
33
+
34
+ def handle_notes(notes)
35
+ story = @document.stories[@current_header][-1]
36
+ if @current_section == :story
37
+ story[:stories][-1][:notes] = notes
38
+ else
39
+ story[:notes] = notes
40
+ end
41
+ end
42
+
27
43
  def handle_definition(definition)
28
44
  @current_section = :definitions
29
45
  @document.definitions[@current_header] ||= []
@@ -39,8 +55,6 @@ module Saga
39
55
  @current_section = :introduction
40
56
  elsif :introduction == @current_section
41
57
  @document.introduction << string
42
- elsif :stories == @current_section and string[0,2] == ' '
43
- @document.stories[@current_header][-1][:notes] = string.strip
44
58
  else
45
59
  @current_header = string.strip
46
60
  end
data/lib/saga/planning.rb CHANGED
@@ -37,6 +37,19 @@ module Saga
37
37
  unestimated
38
38
  end
39
39
 
40
+ def statusses
41
+ statusses = {}
42
+ @document.stories.values.each do |stories|
43
+ stories.each do |story|
44
+ if story[:estimate] and story[:status]
45
+ statusses[story[:status]] ||= 0
46
+ statusses[story[:status]] += self.class.estimate_to_hours(story[:estimate])
47
+ end
48
+ end
49
+ end
50
+ statusses
51
+ end
52
+
40
53
  def to_s
41
54
  if @document.empty?
42
55
  "There are no stories yet."
@@ -49,11 +62,18 @@ module Saga
49
62
  parts << '-'*formatted_totals.length
50
63
  parts << formatted_totals
51
64
  end
52
- parts << self.class.format_unestimated(unestimated) if unestimated > 0
65
+ if unestimated > 0 or !statusses.empty?
66
+ parts << ''
67
+ parts << self.class.format_unestimated(unestimated) if unestimated > 0
68
+ parts << self.class.format_statusses(statusses) unless statusses.empty?
69
+ end
70
+ parts.shift if parts[0] == ''
53
71
  parts.join("\n")
54
72
  end
55
73
  end
56
74
 
75
+ FIRST_COLUMN_WIDTH = 14
76
+
57
77
  def self.estimate_to_hours(estimate)
58
78
  case estimate[1]
59
79
  when :days
@@ -72,11 +92,19 @@ module Saga
72
92
  label = 'Total'
73
93
  end
74
94
  story_column = (properties[:story_count] == 1) ? "#{properties[:story_count]} story" : "#{properties[:story_count]} stories"
75
- "#{label.ljust(14)}: #{properties[:estimate_total_in_hours]} (#{story_column})"
95
+ "#{label.ljust(FIRST_COLUMN_WIDTH)}: #{properties[:estimate_total_in_hours]} (#{story_column})"
76
96
  end
77
97
 
78
98
  def self.format_unestimated(unestimated)
79
99
  "Unestimated : #{unestimated > 1 ? "#{unestimated} stories" : 'one story' }"
80
100
  end
101
+
102
+ def self.format_statusses(statusses)
103
+ parts = []
104
+ statusses.each do |status, hours|
105
+ parts << "#{status.capitalize.ljust(FIRST_COLUMN_WIDTH)}: #{hours}"
106
+ end
107
+ parts.join("\n")
108
+ end
81
109
  end
82
110
  end
@@ -7,6 +7,12 @@ module Saga
7
7
  def process_line(input)
8
8
  if input[0,2].downcase == 'as'
9
9
  @parser.handle_story(self.class.tokenize_story(input))
10
+ elsif input[0,2] == ' '
11
+ @parser.handle_notes(input.strip)
12
+ elsif input[0,3] == '| '
13
+ @parser.handle_notes(input[1..-1].strip)
14
+ elsif input[0,1] == '|'
15
+ @parser.handle_nested_story(self.class.tokenize_story(input[1..-1]))
10
16
  elsif input[0,1] == '-'
11
17
  @parser.handle_author(self.class.tokenize_author(input))
12
18
  elsif input =~ /^\w(\w|[\s-])+:/
@@ -59,6 +65,7 @@ module Saga
59
65
  end
60
66
 
61
67
  def self.tokenize_story(input)
68
+ lines = input.split('\n')
62
69
  parts = input.split(' - ')
63
70
  if parts.length > 1
64
71
  story = tokenize_story_attributes(parts[-1])
@@ -16,6 +16,7 @@
16
16
  table.review td.notes { color: #666; padding: 0 0 .25em 0; font-style: italic; }
17
17
  table.review .dropped { color: #666; text-decoration: line-through; }
18
18
  table.review .done { color: #666; background-color: #f0f8ff; }
19
+ table.review tr.nested td.story { padding-left: 1em; }
19
20
  </style>
20
21
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
21
22
  </head>
@@ -60,6 +61,22 @@
60
61
  <td class="notes" colspan="5"><%= story[:notes] %></td>
61
62
  </tr>
62
63
  <% end %>
64
+ <% if story[:stories] %>
65
+ <% story[:stories].each do |nested| %>
66
+ <tr class="nested <%= nested[:status] %>" id="story<%= nested[:id] %>">
67
+ <td class="story"><%= nested[:description] %></td>
68
+ <td class="meta id"><%= nested[:id] %></td>
69
+ <td class="meta estimate"><%= format_estimate(*nested[:estimate]) if nested[:estimate] %></td>
70
+ <td class="meta iteration"><%= nested[:iteration] %></td>
71
+ <td class="meta status"><%= nested[:status] %></td>
72
+ </tr>
73
+ <% if nested[:notes] %>
74
+ <tr class="nested <%= nested[:status] %>">
75
+ <td class="notes" colspan="5"><%= nested[:notes] %></td>
76
+ </tr>
77
+ <% end %>
78
+ <% end %>
79
+ <% end %>
63
80
  <% end %>
64
81
  </table>
65
82
  <% end %>
@@ -19,7 +19,7 @@ USER STORIES
19
19
  <%= "\n#{header}\n\n" -%>
20
20
  <% end %>
21
21
  <% stories.each do |story| -%>
22
- <%= format_story(story) %>
22
+ <%= format_story(story) -%>
23
23
  <% end -%>
24
24
  <% end -%>
25
25
  <% definitions.each do |header, definitions| -%>
@@ -16,16 +16,22 @@ module Helpers
16
16
  end
17
17
  end
18
18
 
19
- def format_story(story)
19
+ def format_story(story, kind=:regular)
20
20
  story_attributes = []
21
21
  story_attributes << "##{story[:id]}" if story[:id]
22
22
  story_attributes << story[:status] if story[:status]
23
23
  story_attributes << format_estimate(*story[:estimate]) if story[:estimate]
24
24
  story_attributes << "i#{story[:iteration]}" if story[:iteration]
25
25
 
26
- parts = [[story[:description], story_attributes.join(' ')].join(' - ')]
27
- parts << " #{story[:notes]}" if story[:notes]
28
- parts.join("\n")
26
+ prefix = (kind == :nested) ? '| ' : ''
27
+ formatted = "#{prefix}#{story[:description]}"
28
+ formatted << " - #{story_attributes.join(' ')}" unless story_attributes.empty?
29
+ formatted << "\n"
30
+ formatted << "#{prefix} #{story[:notes]}\n" if story[:notes]
31
+ story[:stories].each do |nested|
32
+ formatted << format_story(nested, :nested)
33
+ end if story[:stories]
34
+ formatted
29
35
  end
30
36
 
31
37
  def format_definition(definition)
@@ -0,0 +1,13 @@
1
+ Requirements Case
2
+
3
+ - Manfred Stienstra, manfred@fngtps.com
4
+
5
+ USER STORIES
6
+
7
+ As a writer I would like to write stories so that developers can implement them. - #1 todo
8
+ Stories are written in Saga format.
9
+ | As a writer I would like to add nested stories so that I can write requirements on two zoom levels. - todo
10
+ | Nested stories are basicaly substories of regular stories.
11
+ | As a writer I would like to add header so that I can group stories by theme. - todo
12
+
13
+ Writer: Someone who is responsible for writing down requirements in the form of stories
@@ -0,0 +1,4 @@
1
+ As an editor I would like to manage content so that I can keep the site up to date.
2
+ | As an editor I would like to add a page so that I can create new pages.
3
+ | As an editor I would like to remove a page so that I can remove unwanted pages.
4
+ => {:description => 'As an editor I would like to manage content so that I can keep the site up to date.', :stories => [{:description => 'As an editor I would like to add a page so that I can create new pages.'}, {:description => 'As an editor I would like to remove a page so that I can remove unwanted pages.'}]}
@@ -59,6 +59,11 @@ describe "A Document" do
59
59
 
60
60
  document.stories['Non-functional'] << { :id => 3 }
61
61
  document.used_ids.should == [2, 12, 3]
62
+
63
+ document.stories[''][0][:stories] = [
64
+ {}, {:id => 14}, {}, {:id => 5}
65
+ ]
66
+ document.used_ids.should == [2, 14, 5, 12, 3]
62
67
  end
63
68
 
64
69
  it "returns a list of unused IDs" do
@@ -87,7 +92,9 @@ describe "A Document" do
87
92
 
88
93
  document.stories[''] = []
89
94
  document.stories[''] << { :description => 'First story'}
90
- document.stories[''] << { :description => 'Second story'}
95
+ document.stories[''] << { :description => 'Second story', :stories => [
96
+ { :description => 'First nested story' }, { :id => 15, :description => 'Second nested story'}
97
+ ] }
91
98
 
92
99
  document.stories['Non-functional'] = []
93
100
  document.stories['Non-functional'] << { :id => 1, :description => 'Third story' }
@@ -98,7 +105,8 @@ describe "A Document" do
98
105
 
99
106
  document.autofill_ids
100
107
  document.stories[''].map { |story| story[:id] }.should == [2, 4]
108
+ document.stories[''][1][:stories].map { |story| story[:id] }.should == [5, 15]
101
109
  document.stories['Non-functional'].map { |story| story[:id] }.should == [1]
102
- document.stories['Developer'].map { |story| story[:id] }.should == [5, 3]
110
+ document.stories['Developer'].map { |story| story[:id] }.should == [6, 3]
103
111
  end
104
112
  end
@@ -13,7 +13,12 @@ describe "Formatter" do
13
13
  ]
14
14
  @document.stories = [
15
15
  ['General', [
16
- {:description => 'As a consumer I would like to use TLS (SSL) so that my connection with the API is secure', :id => 4, :status => 'todo', :notes => 'Use a self-signed CA certificate to create the certificates.' }
16
+ {:description => 'As a consumer I would like to use TLS (SSL) so that my connection with the API is secure', :id => 4, :status => 'todo', :notes => 'Use a self-signed CA certificate to create the certificates.', :stories => [
17
+ { :description => 'As a consumer I would like to receive a certificate from the provider.', :id => 12, :status => 'done', :notes => 'The certificate for the CA.' },
18
+ { :description => 'As a consumer I would like to receive a hosts file from the provider.', :id => 13, :status => 'done' }
19
+ ]
20
+ },
21
+ { :description => 'As a consumer I would like to get a list of users', :id => 5, :status => 'todo' }
17
22
  ]]
18
23
  ]
19
24
  end
@@ -21,11 +26,13 @@ describe "Formatter" do
21
26
  it "formats a saga document to HTML" do
22
27
  html = Saga::Formatter.format(@document)
23
28
  html.should.include('<h1>Requirements <br />Requirements API</h1>')
29
+ html.should.include('receive a certificate')
24
30
  end
25
31
 
26
32
  it "formats a saga document to saga" do
27
33
  saga = Saga::Formatter.saga_format(@document)
28
34
  saga.should.include('Requirements Requirements API')
35
+ saga.should.include('receive a certificate')
29
36
  end
30
37
 
31
38
  describe "with an external template" do
@@ -29,10 +29,18 @@ module ParserHelper
29
29
  parser.parse('As a recorder I would like to add a recording so that it becomes available. - #1 todo')
30
30
  end
31
31
 
32
- def parse_story_comment
32
+ def parse_story_notes
33
33
  parser.parse(' “Your recording was created successfully.”')
34
34
  end
35
35
 
36
+ def parse_nested_story
37
+ parser.parse('| As a recorder I would like to add a recording so that it becomes available. - todo')
38
+ end
39
+
40
+ def parse_nested_story_notes
41
+ parser.parse('| “Your recording was created successfully.”')
42
+ end
43
+
36
44
  def parse_definition
37
45
  parser.parse('Other: Stories that don’t fit anywhere else.')
38
46
  end
@@ -43,6 +51,12 @@ describe "Parser" do
43
51
  document = Saga::Parser.parse('')
44
52
  document.should.be.kind_of?(Saga::Document)
45
53
  end
54
+
55
+ it "should initialize and parse a reference document" do
56
+ document = Saga::Parser.parse(File.read(File.expand_path('../cases/document.txt', __FILE__)))
57
+ document.should.be.kind_of?(Saga::Document)
58
+ document.length.should == 3
59
+ end
46
60
  end
47
61
 
48
62
  describe "A Parser, concerning the handling of input" do
@@ -94,7 +108,7 @@ describe "A Parser, concerning the handling of input" do
94
108
  it "interprets a comment after a story as being part of the story" do
95
109
  parse_story_marker
96
110
  parse_story
97
- parse_story_comment
111
+ parse_story_notes
98
112
 
99
113
  parser.document.stories.keys.should == ['']
100
114
  parser.document.stories[''].length.should == 1
@@ -102,6 +116,26 @@ describe "A Parser, concerning the handling of input" do
102
116
  parser.document.stories[''].first[:notes].should == '“Your recording was created successfully.”'
103
117
  end
104
118
 
119
+ it "interprets nested story as part of the parent story" do
120
+ parse_story_marker
121
+ parse_story
122
+ parse_story_notes
123
+ parse_nested_story
124
+ parse_nested_story_notes
125
+ parse_nested_story
126
+ parse_nested_story_notes
127
+
128
+ parser.document.stories.keys.should == ['']
129
+ parser.document.stories[''].length.should == 1
130
+ first_story = parser.document.stories[''][0]
131
+ first_story[:id].should == 1
132
+ first_story[:notes].should == '“Your recording was created successfully.”'
133
+
134
+ first_story[:stories].length.should == 2
135
+ first_story[:stories][0][:notes].should == '“Your recording was created successfully.”'
136
+ first_story[:stories][1][:notes].should == '“Your recording was created successfully.”'
137
+ end
138
+
105
139
  it "interprets definitions without a header as being a gobal definition" do
106
140
  parse_title
107
141
  parse_introduction
@@ -36,6 +36,7 @@ describe "A Planning for estimated and unestimatesd stories" do
36
36
  "Unplanned : 12 (1 story)\n"+
37
37
  "----------------------------\n"+
38
38
  "Total : 12 (1 story)\n"+
39
+ "\n"+
39
40
  "Unestimated : 3 stories"
40
41
  end
41
42
  end
@@ -7,13 +7,13 @@ module CasesHelper
7
7
 
8
8
  def each_case(path)
9
9
  filename = File.expand_path("../cases/#{path}.txt", __FILE__)
10
- input = nil
10
+ input = ''
11
11
  File.readlines(filename).each do |line|
12
- if input.nil?
13
- input = line.strip
14
- else
12
+ if line.start_with?('=>')
15
13
  yield input, _parse_expected(line)
16
- input = nil
14
+ input = ''
15
+ else
16
+ input << "#{line}\n"
17
17
  end
18
18
  end
19
19
  end
@@ -67,6 +67,36 @@ describe "A Tokenizer" do
67
67
  @tokenizer.process_line(line)
68
68
  end
69
69
 
70
+ it "sends a tokenized note to the parser" do
71
+ line = ' Optionally support SSL'
72
+ notes = line.strip
73
+
74
+ @parser.expects(:handle_notes).with(notes)
75
+ @tokenizer.process_line(line)
76
+ end
77
+
78
+ it "doesn't mistake a story note with a semicolon as a definition" do
79
+ line = ' It would be nice if we could use http://www.braintreepaymentsolutions.com/'
80
+ @parser.expects(:handle_notes).with(line.strip)
81
+ @tokenizer.process_line(line)
82
+ end
83
+
84
+ it "sends a nested tokenized story to the parser" do
85
+ line = '| As a recorder I would like to use TLS (SSL) so that my connection with the storage API is secure and I can be sure of the API’s identity. - #4 todo'
86
+ story = Saga::Tokenizer.tokenize_story(line[1..-1])
87
+
88
+ @parser.expects(:handle_nested_story).with(story)
89
+ @tokenizer.process_line(line)
90
+ end
91
+
92
+ it "sends a nested tokenized note to the parser" do
93
+ line = '| Optionally support SSL'
94
+ notes = line[4..-1]
95
+
96
+ @parser.expects(:handle_notes).with(notes)
97
+ @tokenizer.process_line(line)
98
+ end
99
+
70
100
  it "sends a tokenized author to the parser" do
71
101
  line = '- Manfred Stienstra, manfred@fngtps.com'
72
102
  author = Saga::Tokenizer.tokenize_author(line)
@@ -83,12 +113,6 @@ describe "A Tokenizer" do
83
113
  @tokenizer.process_line(line)
84
114
  end
85
115
 
86
- it "doesn't mistake a story note with a semicolon as a definition" do
87
- line = ' It would be nice if we could use http://www.braintreepaymentsolutions.com/'
88
- @parser.expects(:handle_string).with(line)
89
- @tokenizer.process_line(line)
90
- end
91
-
92
116
  it "send a tokenize defintion to the parser (slighly more complex)" do
93
117
  line = 'Search and retrieval: Stories related to selecting and retrieving recordings.'
94
118
  definition = Saga::Tokenizer.tokenize_definition(line)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saga
3
3
  version: !ruby/object:Gem::Version
4
- hash: 1
4
+ hash: 63
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 7
9
- - 1
10
- version: 0.7.1
8
+ - 8
9
+ - 0
10
+ version: 0.8.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Manfred Stienstra
@@ -15,7 +15,8 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-07-04 00:00:00 Z
18
+ date: 2011-10-12 00:00:00 +02:00
19
+ default_executable: saga
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
21
22
  name: erubis
@@ -92,6 +93,8 @@ files:
92
93
  - templates/saga/helpers.rb
93
94
  - test/cases/author.txt
94
95
  - test/cases/definition.txt
96
+ - test/cases/document.txt
97
+ - test/cases/nested_stories.txt
95
98
  - test/cases/story.txt
96
99
  - test/cases/story_attributes.txt
97
100
  - test/fixtures/document.erb
@@ -103,6 +106,7 @@ files:
103
106
  - test/saga_spec.rb
104
107
  - test/saga_tokenizer_spec.rb
105
108
  - test/spec_helper.rb
109
+ has_rdoc: true
106
110
  homepage: http://fingertips.github.com
107
111
  licenses: []
108
112
 
@@ -132,16 +136,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
136
  requirements: []
133
137
 
134
138
  rubyforge_project:
135
- rubygems_version: 1.8.5
139
+ rubygems_version: 1.6.2
136
140
  signing_key:
137
141
  specification_version: 3
138
142
  summary: Saga is a tool to convert stories syntax to a nicely formatted document.
139
- test_files:
140
- - test/saga_document_spec.rb
141
- - test/saga_formatter_spec.rb
142
- - test/saga_parser_spec.rb
143
- - test/saga_planning_spec.rb
144
- - test/saga_runner_spec.rb
145
- - test/saga_spec.rb
146
- - test/saga_tokenizer_spec.rb
147
- - test/spec_helper.rb
143
+ test_files: []
144
+