saga 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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
+