saga 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/.gitignore +2 -0
- data/.kick +14 -0
- data/LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +32 -0
- data/VERSION +1 -0
- data/bin/saga +5 -0
- data/lib/saga.rb +13 -0
- data/lib/saga/document.rb +13 -0
- data/lib/saga/formatter.rb +32 -0
- data/lib/saga/parser.rb +51 -0
- data/lib/saga/runner.rb +41 -0
- data/lib/saga/tokenizer.rb +71 -0
- data/templates/default/document.erb +71 -0
- data/templates/default/helpers.rb +17 -0
- data/test/cases/author.txt +8 -0
- data/test/cases/definition.txt +10 -0
- data/test/cases/story.txt +4 -0
- data/test/cases/story_attributes.txt +10 -0
- data/test/saga_document_spec.rb +10 -0
- data/test/saga_formatter_spec.rb +13 -0
- data/test/saga_parser_spec.rb +112 -0
- data/test/saga_runner_spec.rb +16 -0
- data/test/saga_tokenizer_spec.rb +105 -0
- data/test/spec_helper.rb +10 -0
- metadata +85 -0
data/.gitignore
ADDED
data/.kick
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
recipe :ignore
|
2
|
+
ignore(/(^tmp|\.DS_Store)$/)
|
3
|
+
|
4
|
+
recipe :ruby
|
5
|
+
process do |files|
|
6
|
+
Ruby.run_tests(files.take_and_map do |file|
|
7
|
+
case file
|
8
|
+
when %r{^lib/(.+)\.rb$}
|
9
|
+
"test/#{$1.gsub('/', '_')}_spec.rb"
|
10
|
+
when %r{^test/(.+)_spec\.rb$}
|
11
|
+
file
|
12
|
+
end
|
13
|
+
end)
|
14
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Fingertips, Manfred Stienstra <manfred@fngtps.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
|
4
|
+
desc "Run all specs by default"
|
5
|
+
task :default => [:spec]
|
6
|
+
|
7
|
+
desc "Run all specs"
|
8
|
+
task :spec do
|
9
|
+
Dir[File.dirname(__FILE__) + '/test/**/*_spec.rb'].each do |file|
|
10
|
+
load file
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
namespace :documentation do
|
15
|
+
Rake::RDocTask.new(:generate) do |rd|
|
16
|
+
rd.main = "README.rdoc"
|
17
|
+
rd.rdoc_files.include("README.rdoc", "LICENSE", "bin/**/*.rb", "lib/**/*.rb", "templates/**/*.rb")
|
18
|
+
rd.options << "--all" << "--charset" << "utf-8"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
require 'jeweler'
|
24
|
+
Jeweler::Tasks.new do |s|
|
25
|
+
s.name = "saga"
|
26
|
+
s.summary = s.description = "Saga is a tool to convert stories syntax to a nicely formatted document."
|
27
|
+
s.homepage = "http://fingertips.github.com"
|
28
|
+
s.email = "manfred@fngtps.com"
|
29
|
+
s.authors = ["Manfred Stienstra"]
|
30
|
+
end
|
31
|
+
rescue LoadError
|
32
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/saga
ADDED
data/lib/saga.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Saga
|
2
|
+
autoload :Document, 'saga/document'
|
3
|
+
autoload :Formatter, 'saga/formatter'
|
4
|
+
autoload :Parser, 'saga/parser'
|
5
|
+
autoload :Runner, 'saga/runner'
|
6
|
+
autoload :Tokenizer, 'saga/tokenizer'
|
7
|
+
|
8
|
+
def self.run(argv)
|
9
|
+
runner = ::Saga::Runner.new(argv)
|
10
|
+
runner.run
|
11
|
+
runner
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'erubis'
|
3
|
+
|
4
|
+
module Saga
|
5
|
+
class Formatter
|
6
|
+
TEMPLATE_PATH = File.expand_path('../../../templates', __FILE__)
|
7
|
+
|
8
|
+
def initialize(document)
|
9
|
+
@document = document
|
10
|
+
end
|
11
|
+
|
12
|
+
def format
|
13
|
+
helpers_file = File.join(self.class.template_path, 'default/helpers.rb')
|
14
|
+
load helpers_file
|
15
|
+
@document.extend(Helpers)
|
16
|
+
binding = @document.send(:binding)
|
17
|
+
|
18
|
+
template_file = File.join(self.class.template_path, 'default/document.erb')
|
19
|
+
template = Erubis::Eruby.new(File.read(template_file))
|
20
|
+
template.result(binding)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.format(document)
|
24
|
+
formatter = new(document)
|
25
|
+
formatter.format
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.template_path
|
29
|
+
TEMPLATE_PATH
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/saga/parser.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Saga
|
2
|
+
class Parser
|
3
|
+
attr_accessor :document
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@tokenizer = ::Saga::Tokenizer.new(self)
|
7
|
+
@document = ::Saga::Document.new
|
8
|
+
@current_section = :title
|
9
|
+
@current_header = ''
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(input)
|
13
|
+
@tokenizer.process(input)
|
14
|
+
@document
|
15
|
+
end
|
16
|
+
|
17
|
+
def handle_author(author)
|
18
|
+
@document.authors << author
|
19
|
+
end
|
20
|
+
|
21
|
+
def handle_story(story)
|
22
|
+
@document.stories[@current_header] ||= []
|
23
|
+
@document.stories[@current_header] << story
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_definition(definition)
|
27
|
+
@document.definitions[@current_header] ||= []
|
28
|
+
@document.definitions[@current_header] << definition
|
29
|
+
end
|
30
|
+
|
31
|
+
def handle_string(string)
|
32
|
+
return if string.strip == ''
|
33
|
+
return(@current_section = :body) if string.strip == 'USER STORIES'
|
34
|
+
|
35
|
+
case @current_section
|
36
|
+
when :title
|
37
|
+
@document.title = string.gsub(/^requirements/i, '').strip
|
38
|
+
@current_section = :introduction
|
39
|
+
when :introduction
|
40
|
+
@document.introduction << string
|
41
|
+
else
|
42
|
+
@current_header = string.strip
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.parse(input)
|
47
|
+
parser = new
|
48
|
+
parser.parse(input)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/saga/runner.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Saga
|
4
|
+
class Runner
|
5
|
+
def initialize(argv)
|
6
|
+
@argv = argv
|
7
|
+
@options = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def parser
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: saga [command] [options] <file>"
|
13
|
+
opts.separator ""
|
14
|
+
opts.on("-h", "--help", "Show help") do
|
15
|
+
puts opts
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_command(command, options)
|
22
|
+
case command
|
23
|
+
when :run
|
24
|
+
else
|
25
|
+
filename = File.expand_path(command)
|
26
|
+
document = Saga::Parser.parse(File.read(filename))
|
27
|
+
puts Saga::Formatter.format(document)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def run
|
32
|
+
argv = @argv.dup
|
33
|
+
parser.parse!(argv)
|
34
|
+
if command = argv.shift
|
35
|
+
run_command(command, @options)
|
36
|
+
else
|
37
|
+
puts parser.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Saga
|
2
|
+
class Tokenizer
|
3
|
+
def initialize(parser)
|
4
|
+
@parser = parser
|
5
|
+
end
|
6
|
+
|
7
|
+
def process_line(input)
|
8
|
+
if input[0,2].downcase == 'as'
|
9
|
+
@parser.handle_story(self.class.tokenize_story(input))
|
10
|
+
elsif input[0,1] == '-'
|
11
|
+
@parser.handle_author(self.class.tokenize_author(input))
|
12
|
+
elsif input =~ /^(\w|[\s-])+:/
|
13
|
+
@parser.handle_definition(self.class.tokenize_definition(input))
|
14
|
+
else
|
15
|
+
@parser.handle_string(input)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def process(input)
|
20
|
+
input.split("\n").each do |line|
|
21
|
+
process_line(line)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.tokenize_story_attributes(input)
|
26
|
+
return {} if input.nil?
|
27
|
+
|
28
|
+
attributes = {}
|
29
|
+
rest = []
|
30
|
+
parts = input.split(/\s/)
|
31
|
+
|
32
|
+
parts.each do |part|
|
33
|
+
if part.strip == ''
|
34
|
+
next
|
35
|
+
elsif match = /\#(\d+)/.match(part)
|
36
|
+
attributes[:id] = match[1].to_i
|
37
|
+
else
|
38
|
+
rest << part
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
attributes[:status] = rest.join(' ') unless rest.empty?
|
43
|
+
attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.tokenize_story(input)
|
47
|
+
description, attributes = input.split('-')
|
48
|
+
story = tokenize_story_attributes(attributes)
|
49
|
+
story[:description] = description.strip
|
50
|
+
story
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.tokenize_definition(input)
|
54
|
+
if match = /^([^:]+)\s*:\s*(.+)\s*$/.match(input)
|
55
|
+
{:title => match[1], :definition => match[2]}
|
56
|
+
else
|
57
|
+
{}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.tokenize_author(input)
|
62
|
+
author = {}
|
63
|
+
parts = input[1..-1].split(',')
|
64
|
+
author[:name] = parts[0].strip if parts[0]
|
65
|
+
author[:email] = parts[1].strip if parts[1]
|
66
|
+
author[:company] = parts[2].strip if parts[2]
|
67
|
+
author[:website] = parts[3].strip if parts[3]
|
68
|
+
author
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
4
|
+
<head>
|
5
|
+
<title>Requirements <%= title %> · Fingertips</title>
|
6
|
+
<link rel="stylesheet" type="text/css" media="all" href="http://resources.fngtps.com/2006/doc.css" />
|
7
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
8
|
+
<style type="text/css">
|
9
|
+
td.id { text-align: right !important; }
|
10
|
+
td.id a { color: #000; }
|
11
|
+
table.review { border-bottom: 1px solid #ccc; margin-top: -1em;}
|
12
|
+
table.review td { padding: .25em 0; vertical-align: top; text-align: left; min-width: 1em; }
|
13
|
+
table.review th { padding: .25em 0; vertical-align: top; text-align: right; }
|
14
|
+
table.review td.story { width: 100%; border-top: 1px solid #ccc; }
|
15
|
+
table.review td.meta { padding-left: 1em; border-top: 1px solid #ccc; text-align: right !important; white-space: nowrap; }
|
16
|
+
table.review td.notes { color: #666; padding: 0 0 .25em 0; font-style: italic; }
|
17
|
+
table.review .dropped { color: #666; text-decoration: line-through; }
|
18
|
+
table.review .done { color: #666; background-color: #f0f8ff; }
|
19
|
+
</style>
|
20
|
+
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
|
21
|
+
</head>
|
22
|
+
<body id="doc" class="requirements">
|
23
|
+
|
24
|
+
<p id="logo"><img src="http://resources.fngtps.com/2006/print-logo.png" alt="Fingertips design & development" /> </p>
|
25
|
+
|
26
|
+
<h1>Requirements <br /><%= title %></h1>
|
27
|
+
|
28
|
+
<% authors.each do |author| %>
|
29
|
+
<p class="author"><%= format_author(author) %></p>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
<% unless stories.empty? %>
|
33
|
+
<h2>User stories</h2>
|
34
|
+
<% stories.each do |header, stories| %>
|
35
|
+
<% unless header.strip == '' %>
|
36
|
+
<h3><%= header %></h3>
|
37
|
+
<% end %>
|
38
|
+
<table class="review">
|
39
|
+
<tr>
|
40
|
+
<th></th>
|
41
|
+
<th title="id">#</th>
|
42
|
+
<th title="Estimate">e</th>
|
43
|
+
<th title="Iteration">i</th>
|
44
|
+
<th title="Status">s</th>
|
45
|
+
</tr>
|
46
|
+
<% stories.each do |story| %>
|
47
|
+
<tr class="<%= story[:status] %>" id="story<%= story[:id] %>">
|
48
|
+
<td class="story"><%= story[:description] %></td>
|
49
|
+
<td class="meta id"><%= story[:id] %></td>
|
50
|
+
<td class="meta estimate"><%= story[:estimate] %></td>
|
51
|
+
<td class="meta iteration"><%= story[:iteration] %></td>
|
52
|
+
<td class="meta status"><%= story[:status] %></td>
|
53
|
+
</tr>
|
54
|
+
<% end %>
|
55
|
+
</table>
|
56
|
+
<% end %>
|
57
|
+
<% end %>
|
58
|
+
|
59
|
+
<% definitions.each do |header, definitions| %>
|
60
|
+
<% unless header.strip == '' %>
|
61
|
+
<h2><%= format_header(header) %></h2>
|
62
|
+
<% end %>
|
63
|
+
<dl>
|
64
|
+
<% definitions.each do |definition| %>
|
65
|
+
<dt><%= definition[:title] %></dt>
|
66
|
+
<dd><%= definition[:definition] %></dd>
|
67
|
+
<% end %>
|
68
|
+
</dl>
|
69
|
+
<% end %>
|
70
|
+
</body>
|
71
|
+
</html>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Helpers
|
2
|
+
def format_author(author)
|
3
|
+
parts = []
|
4
|
+
parts << author[:name] if author[:name]
|
5
|
+
parts << "<a href=\"mailto:#{author[:email]}\">#{author[:email]}</a>" if author[:email]
|
6
|
+
if author[:website] and author[:company]
|
7
|
+
parts << "<a href=\"#{author[:website]}\">#{author[:company]}</a>"
|
8
|
+
elsif author[:company]
|
9
|
+
parts << author[:company]
|
10
|
+
end
|
11
|
+
parts.join(', ')
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_header(header)
|
15
|
+
"#{header[0,1].upcase}#{header[1..-1].downcase}"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
- Thijs van der Vossen
|
2
|
+
=> {:name => 'Thijs van der Vossen'}
|
3
|
+
- Thijs van der Vossen, thijs@fngtps.com
|
4
|
+
=> {:name => 'Thijs van der Vossen', :email => 'thijs@fngtps.com'}
|
5
|
+
- Thijs van der Vossen, thijs@fngtps.com, Fingertips
|
6
|
+
=> {:name => 'Thijs van der Vossen', :email => 'thijs@fngtps.com', :company => 'Fingertips'}
|
7
|
+
- Thijs van der Vossen, thijs@fngtps.com, Fingertips, http://www.fngtps.com
|
8
|
+
=> {:name => 'Thijs van der Vossen', :email => 'thijs@fngtps.com', :company => 'Fingertips', :website => 'http://www.fngtps.com'}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
=> {}
|
3
|
+
User: A user
|
4
|
+
=> {:title => 'User', :definition => 'A user'}
|
5
|
+
Administrator: Any user with full administrative access.
|
6
|
+
=> {:title => 'Administrator', :definition => 'Any user with full administrative access.'}
|
7
|
+
Non-functional: Stories related to non-functional requirements.
|
8
|
+
=> {:title => 'Non-functional', :definition => 'Stories related to non-functional requirements.'}
|
9
|
+
Service: The part of the web service used by clients to provide search and retrieval as well as account, device, and user management.
|
10
|
+
=> {:title => 'Service', :definition => 'The part of the web service used by clients to provide search and retrieval as well as account, device, and user management.'}
|
@@ -0,0 +1,4 @@
|
|
1
|
+
As a developer I would like to have created database indexes so that the queries run as fast as possible.
|
2
|
+
=> {:description => 'As a developer I would like to have created database indexes so that the queries run as fast as possible.'}
|
3
|
+
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
|
4
|
+
=> {:description => '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.', :id => 4, :status => 'todo' }
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "A Document" do
|
4
|
+
it "responds to accessors" do
|
5
|
+
document = Saga::Document.new
|
6
|
+
[:title, :introduction, :authors, :stories, :definitions].each do |method|
|
7
|
+
document.should.respond_to(method)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "Formatter" do
|
4
|
+
before do
|
5
|
+
@document = Saga::Document.new
|
6
|
+
@document.title = 'Requirements API'
|
7
|
+
end
|
8
|
+
|
9
|
+
it "formats a saga document to HTML" do
|
10
|
+
html = Saga::Formatter.format(@document)
|
11
|
+
html.should.include('<h1>Requirements <br />Requirements API</h1>')
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
module ParserHelper
|
4
|
+
def parser
|
5
|
+
@parser ||= Saga::Parser.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def parse(input)
|
9
|
+
Saga::Parser.parse(input)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_title
|
13
|
+
parser.parse("Requirements API\n\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_introduction
|
17
|
+
parser.parse("This document describes our API.\n\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_story_marker
|
21
|
+
parser.parse('USER STORIES')
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_header
|
25
|
+
parser.parse('Storage')
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_story
|
29
|
+
parser.parse('As a recorder I would like to add a recording so that it becomes available. - #1 todo')
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_definition
|
33
|
+
parser.parse('Other: Stories that don’t fit anywhere else.')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "Parser" do
|
38
|
+
it "should initialize and parse" do
|
39
|
+
document = Saga::Parser.parse('')
|
40
|
+
document.should.be.kind_of?(Saga::Document)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "A Parser, concerning the handling of input" do
|
45
|
+
extend ParserHelper
|
46
|
+
before { @parser = nil }
|
47
|
+
|
48
|
+
it "interprets the first line of the document as a title" do
|
49
|
+
document = parse('Requirements API')
|
50
|
+
document.title.should == 'API'
|
51
|
+
|
52
|
+
document = parse('Record and Search')
|
53
|
+
document.title.should == 'Record and Search'
|
54
|
+
end
|
55
|
+
|
56
|
+
it "interprets authors" do
|
57
|
+
document = parse('- Manfred Stienstra')
|
58
|
+
document.authors.map { |author| author[:name] }.should == ['Manfred Stienstra']
|
59
|
+
end
|
60
|
+
|
61
|
+
it "interprets the introduction" do
|
62
|
+
parse_title
|
63
|
+
parse_introduction
|
64
|
+
parser.document.introduction.should == ['This document describes our API.']
|
65
|
+
end
|
66
|
+
|
67
|
+
it "interprets stories without a header as being a gobal story" do
|
68
|
+
parse_title
|
69
|
+
parse_introduction
|
70
|
+
parse_story_marker
|
71
|
+
parse_story
|
72
|
+
|
73
|
+
parser.document.stories.keys.should == ['']
|
74
|
+
parser.document.stories[''].length.should == 1
|
75
|
+
parser.document.stories[''].first[:id].should == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
it "interprets stories with a header as being part of that section" do
|
79
|
+
parse_title
|
80
|
+
parse_introduction
|
81
|
+
parse_story_marker
|
82
|
+
parse_header
|
83
|
+
parse_story
|
84
|
+
|
85
|
+
parser.document.stories.keys.should == ['Storage']
|
86
|
+
parser.document.stories['Storage'].length.should == 1
|
87
|
+
parser.document.stories['Storage'].first[:id].should == 1
|
88
|
+
end
|
89
|
+
|
90
|
+
it "interprets definitions without a header as being a gobal definition" do
|
91
|
+
parse_title
|
92
|
+
parse_introduction
|
93
|
+
parse_story_marker
|
94
|
+
parse_definition
|
95
|
+
|
96
|
+
parser.document.definitions.keys.should == ['']
|
97
|
+
parser.document.definitions[''].length.should == 1
|
98
|
+
parser.document.definitions[''].first[:title].should == 'Other'
|
99
|
+
end
|
100
|
+
|
101
|
+
it "interprets definitions with a header as being part of that section" do
|
102
|
+
parse_title
|
103
|
+
parse_introduction
|
104
|
+
parse_story_marker
|
105
|
+
parse_header
|
106
|
+
parse_definition
|
107
|
+
|
108
|
+
parser.document.definitions.keys.should == ['Storage']
|
109
|
+
parser.document.definitions['Storage'].length.should == 1
|
110
|
+
parser.document.definitions['Storage'].first[:title].should == 'Other'
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "A Runner" do
|
4
|
+
it "should show a help text when invoked without a command and options" do
|
5
|
+
@runner = Saga::Runner.new([])
|
6
|
+
@runner.expects(:puts).with(@runner.parser.to_s)
|
7
|
+
@runner.run
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should show a help test when the -h option is used" do
|
11
|
+
@runner = Saga::Runner.new(%w(-h))
|
12
|
+
@runner.expects(:puts).at_least(1)
|
13
|
+
@runner.stubs(:exit)
|
14
|
+
@runner.run
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
module CasesHelper
|
4
|
+
def _parse_expected(line)
|
5
|
+
eval(line[3..-1])
|
6
|
+
end
|
7
|
+
|
8
|
+
def each_case(path)
|
9
|
+
filename = File.expand_path("../cases/#{path}.txt", __FILE__)
|
10
|
+
input = nil
|
11
|
+
File.readlines(filename).each do |line|
|
12
|
+
if input.nil?
|
13
|
+
input = line.strip
|
14
|
+
else
|
15
|
+
yield input, _parse_expected(line)
|
16
|
+
input = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "Tokenizer" do
|
23
|
+
extend CasesHelper
|
24
|
+
|
25
|
+
it "tokenizes story attributes input" do
|
26
|
+
each_case('story_attributes') do |input, expected|
|
27
|
+
Saga::Tokenizer.tokenize_story_attributes(input).should == expected
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it "tokenizes story input" do
|
32
|
+
each_case('story') do |input, expected|
|
33
|
+
Saga::Tokenizer.tokenize_story(input).should == expected
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "tokenizes definition input" do
|
38
|
+
each_case('definition') do |input, expected|
|
39
|
+
Saga::Tokenizer.tokenize_definition(input).should == expected
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it "tokenizes author input" do
|
44
|
+
each_case('author') do |input, expected|
|
45
|
+
Saga::Tokenizer.tokenize_author(input).should == expected
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "A Tokenizer" do
|
51
|
+
before do
|
52
|
+
@parser = stub('Parser')
|
53
|
+
@tokenizer = Saga::Tokenizer.new(@parser)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "sends a tokenized story to the parser" do
|
57
|
+
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'
|
58
|
+
story = Saga::Tokenizer.tokenize_story(line)
|
59
|
+
|
60
|
+
@parser.expects(:handle_story).with(story)
|
61
|
+
@tokenizer.process_line(line)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "sends a tokenized author to the parser" do
|
65
|
+
line = '- Manfred Stienstra, manfred@fngtps.com'
|
66
|
+
author = Saga::Tokenizer.tokenize_author(line)
|
67
|
+
|
68
|
+
@parser.expects(:handle_author).with(author)
|
69
|
+
@tokenizer.process_line(line)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "sends a tokenized definition to the parser" do
|
73
|
+
line = 'Author: Someone who writes'
|
74
|
+
definition = Saga::Tokenizer.tokenize_definition(line)
|
75
|
+
|
76
|
+
@parser.expects(:handle_definition).with(definition)
|
77
|
+
@tokenizer.process_line(line)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "send a tokenize defintion to the parser (slighly more complex)" do
|
81
|
+
line = 'Search and retrieval: Stories related to selecting and retrieving recordings.'
|
82
|
+
definition = Saga::Tokenizer.tokenize_definition(line)
|
83
|
+
|
84
|
+
@parser.expects(:handle_definition).with(definition)
|
85
|
+
@tokenizer.process_line(line)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "forwards plain strings to the parser" do
|
89
|
+
[
|
90
|
+
'Requirements User Application',
|
91
|
+
'USER STORIES',
|
92
|
+
''
|
93
|
+
].each do |line|
|
94
|
+
@parser.expects(:handle_string).with(line)
|
95
|
+
@tokenizer.process_line(line)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "processes lines from the input" do
|
100
|
+
input = File.read(File.expand_path("../cases/story.txt", __FILE__))
|
101
|
+
count = input.split("\n").length
|
102
|
+
@tokenizer.expects(:process_line).times(count)
|
103
|
+
@tokenizer.process(input)
|
104
|
+
end
|
105
|
+
end
|
data/test/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: saga
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Manfred Stienstra
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-31 00:00:00 +02:00
|
13
|
+
default_executable: saga
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Saga is a tool to convert stories syntax to a nicely formatted document.
|
17
|
+
email: manfred@fngtps.com
|
18
|
+
executables:
|
19
|
+
- saga
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- .kick
|
28
|
+
- LICENSE
|
29
|
+
- README.rdoc
|
30
|
+
- Rakefile
|
31
|
+
- VERSION
|
32
|
+
- bin/saga
|
33
|
+
- lib/saga.rb
|
34
|
+
- lib/saga/document.rb
|
35
|
+
- lib/saga/formatter.rb
|
36
|
+
- lib/saga/parser.rb
|
37
|
+
- lib/saga/runner.rb
|
38
|
+
- lib/saga/tokenizer.rb
|
39
|
+
- templates/default/document.erb
|
40
|
+
- templates/default/helpers.rb
|
41
|
+
- test/cases/author.txt
|
42
|
+
- test/cases/definition.txt
|
43
|
+
- test/cases/story.txt
|
44
|
+
- test/cases/story_attributes.txt
|
45
|
+
- test/saga_document_spec.rb
|
46
|
+
- test/saga_formatter_spec.rb
|
47
|
+
- test/saga_parser_spec.rb
|
48
|
+
- test/saga_runner_spec.rb
|
49
|
+
- test/saga_tokenizer_spec.rb
|
50
|
+
- test/spec_helper.rb
|
51
|
+
has_rdoc: true
|
52
|
+
homepage: http://fingertips.github.com
|
53
|
+
licenses: []
|
54
|
+
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options:
|
57
|
+
- --charset=UTF-8
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.3.5
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Saga is a tool to convert stories syntax to a nicely formatted document.
|
79
|
+
test_files:
|
80
|
+
- test/saga_document_spec.rb
|
81
|
+
- test/saga_formatter_spec.rb
|
82
|
+
- test/saga_parser_spec.rb
|
83
|
+
- test/saga_runner_spec.rb
|
84
|
+
- test/saga_tokenizer_spec.rb
|
85
|
+
- test/spec_helper.rb
|