doc_storage 0.9 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +23 -31
- data/Rakefile +3 -3
- data/VERSION +1 -1
- data/examples/multipart.rb +4 -8
- data/examples/simple.rb +3 -7
- data/lib/doc_storage/{multi_part_document.rb → multipart_document.rb} +58 -31
- data/lib/doc_storage/simple_document.rb +116 -30
- data/lib/doc_storage.rb +1 -1
- data/spec/multipart_document_spec.rb +215 -0
- data/spec/simple_document_spec.rb +169 -30
- metadata +5 -5
- data/spec/multi_part_document_spec.rb +0 -139
data/README.rdoc
CHANGED
@@ -1,21 +1,19 @@
|
|
1
1
|
= DocStorage
|
2
2
|
|
3
|
-
http://
|
3
|
+
{http://bitbucket.org/dmajda/doc_storage/}[http://bitbucket.org/dmajda/doc_storage/]
|
4
4
|
|
5
|
-
DocStorage is a simple Ruby library for manipulating documents containing
|
5
|
+
DocStorage is a simple Ruby library for manipulating documents containing text
|
6
6
|
and metadata. These documents can be used to implement a blog, wiki, or similar
|
7
|
-
application without a
|
7
|
+
application without a database.
|
8
8
|
|
9
|
-
|
10
|
-
documents</em>. A simple document looks like a RFC 822 message and it is
|
11
|
-
suitable for storing a text associated with some metadata (e.g. a blog article
|
12
|
-
with a title and a publication date). A multipart document is loosely based on
|
13
|
-
the MIME multipart message format and allows storing multiple simple documents
|
14
|
-
(e.g. blog comments, each with an author and a publication date) in one file.
|
9
|
+
== Document Formats
|
15
10
|
|
16
|
-
|
11
|
+
The library distinguishes between <em>simple documents</em> and <em>multipart
|
12
|
+
documents</em>.
|
17
13
|
|
18
|
-
A simple document
|
14
|
+
A simple document is similar to a RFC 822 message and it is suitable for storing
|
15
|
+
text associated with some metadata (e.g. a blog article with a title and a
|
16
|
+
publication date). It looks like this:
|
19
17
|
|
20
18
|
Title: My blog article
|
21
19
|
Datetime: 2009-11-01 18:03:27
|
@@ -25,8 +23,9 @@ A simple document looks like this:
|
|
25
23
|
Suspendisse metus sapien, consectetur vitae imperdiet vel, ornare a metus.
|
26
24
|
In imperdiet euismod mi, nec volutpat lorem porta id.
|
27
25
|
|
28
|
-
|
29
|
-
|
26
|
+
A multipart document is loosely based on the MIME multipart message format and
|
27
|
+
allows storing multiple simple documents (e.g. blog comments, each with an
|
28
|
+
author and a publication date) in one file. It looks like this:
|
30
29
|
|
31
30
|
Boundary: =====
|
32
31
|
|
@@ -42,12 +41,12 @@ A multipart document looks like this:
|
|
42
41
|
Your article sucks!
|
43
42
|
|
44
43
|
See the documentation of <tt>DocStorage::SimpleDocument</tt> and
|
45
|
-
<tt>DocStorage::
|
44
|
+
<tt>DocStorage::MultipartDocument</tt> classes for more formal format
|
46
45
|
description.
|
47
46
|
|
48
47
|
== Installation
|
49
48
|
|
50
|
-
sudo gem install doc_storage --source http://gemcutter.org
|
49
|
+
sudo gem install doc_storage --source http://gemcutter.org/
|
51
50
|
|
52
51
|
== Example Usage
|
53
52
|
|
@@ -64,25 +63,21 @@ description.
|
|
64
63
|
"We should finish the documentation ASAP."
|
65
64
|
)
|
66
65
|
|
67
|
-
#
|
68
|
-
document =
|
69
|
-
DocStorage::SimpleDocument.parse(f)
|
70
|
-
end
|
66
|
+
# Load from a file
|
67
|
+
document = DocStorage::SimpleDocument.load_file("examples/simple.txt")
|
71
68
|
|
72
69
|
# Document manipulation
|
73
70
|
document.headers["Tags"] = "example"
|
74
71
|
document.body += "Nulla mi dui, pellentesque et accumsan vitae, mattis et velit."
|
75
72
|
|
76
73
|
# Save the modified document
|
77
|
-
|
78
|
-
f.write(document)
|
79
|
-
end
|
74
|
+
document.save_file("examples/simple_modified.txt")
|
80
75
|
|
81
76
|
=== Multipart Documents
|
82
77
|
require "lib/doc_storage"
|
83
78
|
|
84
79
|
# Create a new document with two parts
|
85
|
-
document = DocStorage::
|
80
|
+
document = DocStorage::MultipartDocument.new([
|
86
81
|
DocStorage::SimpleDocument.new(
|
87
82
|
{
|
88
83
|
"Title" => "Finishing the documentation",
|
@@ -99,10 +94,8 @@ description.
|
|
99
94
|
),
|
100
95
|
])
|
101
96
|
|
102
|
-
#
|
103
|
-
document =
|
104
|
-
DocStorage::MultiPartDocument.parse(f)
|
105
|
-
end
|
97
|
+
# Load from a file
|
98
|
+
document = DocStorage::MultipartDocument.load_file("examples/multipart.txt")
|
106
99
|
|
107
100
|
# Document manipulation
|
108
101
|
document.parts << DocStorage::SimpleDocument.new(
|
@@ -114,10 +107,9 @@ description.
|
|
114
107
|
)
|
115
108
|
|
116
109
|
# Save the modified document
|
117
|
-
|
118
|
-
f.write(document)
|
119
|
-
end
|
110
|
+
document.save_file("examples/multipart_modified.txt")
|
120
111
|
|
121
112
|
== Author
|
122
113
|
|
123
|
-
DocStorage was brought to you by David Majda
|
114
|
+
DocStorage was brought to you by David Majda
|
115
|
+
(david@majda.cz[mailto:david@majda.cz], majda.cz[http://majda.cz/]).
|
data/Rakefile
CHANGED
@@ -3,7 +3,7 @@ require "rake/rdoctask"
|
|
3
3
|
require "spec/rake/spectask"
|
4
4
|
|
5
5
|
Spec::Rake::SpecTask.new do |t|
|
6
|
-
t.spec_opts = ["--color"
|
6
|
+
t.spec_opts = ["--color"]
|
7
7
|
end
|
8
8
|
|
9
9
|
Rake::RDocTask.new do |t|
|
@@ -14,7 +14,7 @@ end
|
|
14
14
|
|
15
15
|
specification = Gem::Specification.new do |s|
|
16
16
|
s.name = "doc_storage"
|
17
|
-
s.version = "0
|
17
|
+
s.version = "1.0"
|
18
18
|
s.summary = "Simple Ruby library for manipulating documents containing a " +
|
19
19
|
"text and metadata."
|
20
20
|
s.description = "DocStorage is a simple Ruby library for manipulating " +
|
@@ -25,7 +25,7 @@ specification = Gem::Specification.new do |s|
|
|
25
25
|
|
26
26
|
s.author = "David Majda"
|
27
27
|
s.email = "david@majda.cz"
|
28
|
-
s.homepage = "http://
|
28
|
+
s.homepage = "http://bitbucket.org/dmajda/doc_storage/"
|
29
29
|
|
30
30
|
s.files = FileList[
|
31
31
|
"Rakefile",
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0
|
1
|
+
1.0
|
data/examples/multipart.rb
CHANGED
@@ -3,7 +3,7 @@ dir = File.dirname(__FILE__)
|
|
3
3
|
require "#{dir}/../lib/doc_storage"
|
4
4
|
|
5
5
|
# Create a new document with two parts
|
6
|
-
document = DocStorage::
|
6
|
+
document = DocStorage::MultipartDocument.new([
|
7
7
|
DocStorage::SimpleDocument.new(
|
8
8
|
{
|
9
9
|
"Title" => "Finishing the documentation",
|
@@ -20,10 +20,8 @@ document = DocStorage::MultiPartDocument.new([
|
|
20
20
|
),
|
21
21
|
])
|
22
22
|
|
23
|
-
#
|
24
|
-
document =
|
25
|
-
DocStorage::MultiPartDocument.parse(f)
|
26
|
-
end
|
23
|
+
# Load from a file
|
24
|
+
document = DocStorage::MultipartDocument.load_file("examples/multipart.txt")
|
27
25
|
|
28
26
|
# Document manipulation
|
29
27
|
document.parts << DocStorage::SimpleDocument.new(
|
@@ -35,6 +33,4 @@ document.parts << DocStorage::SimpleDocument.new(
|
|
35
33
|
)
|
36
34
|
|
37
35
|
# Save the modified document
|
38
|
-
|
39
|
-
f.write(document)
|
40
|
-
end
|
36
|
+
document.save_file("#{dir}/multipart_modified.txt")
|
data/examples/simple.rb
CHANGED
@@ -11,16 +11,12 @@ document = DocStorage::SimpleDocument.new(
|
|
11
11
|
"We should finish the documentation ASAP."
|
12
12
|
)
|
13
13
|
|
14
|
-
#
|
15
|
-
document =
|
16
|
-
DocStorage::SimpleDocument.parse(f)
|
17
|
-
end
|
14
|
+
# Load from a file
|
15
|
+
document = DocStorage::SimpleDocument.load_file("examples/simple.txt")
|
18
16
|
|
19
17
|
# Document manipulation
|
20
18
|
document.headers["Tags"] = "example"
|
21
19
|
document.body += "Nulla mi dui, pellentesque et accumsan vitae, mattis et velit."
|
22
20
|
|
23
21
|
# Save the modified document
|
24
|
-
|
25
|
-
f.write(document)
|
26
|
-
end
|
22
|
+
document.save_file("#{dir}/simple_modified.txt")
|
@@ -1,12 +1,11 @@
|
|
1
1
|
module DocStorage
|
2
|
-
# The +
|
2
|
+
# The +MultipartDocument+ class represents a document consisting of several
|
3
3
|
# simple documents (see the +SimpleDocument+ class documentation for a
|
4
4
|
# description), loosely based on the MIME multipart message format. It is
|
5
|
-
# suitable for storing multiple documents containing
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# to a file.
|
5
|
+
# suitable for storing multiple documents containing text associated with some
|
6
|
+
# metadata (e.g. blog comments, each with an author and a publication date).
|
7
|
+
# The +MultipartDocument+ class allows to create the document programatically,
|
8
|
+
# load it from a file, manipulate its structure and save it to a file.
|
10
9
|
#
|
11
10
|
# == Document Format
|
12
11
|
#
|
@@ -30,16 +29,16 @@ module DocStorage
|
|
30
29
|
# <em>boundary string</em>. The first document is a _prologue_ and it defines
|
31
30
|
# the boundary string (without the "--" prefix) in its "Boundary" header. All
|
32
31
|
# other headers of the prologue are ignored and so is its body. Remaining
|
33
|
-
# documents are
|
34
|
-
#
|
35
|
-
#
|
32
|
+
# documents are _parts_ of the multipart document. Documents without any parts
|
33
|
+
# are perfectly legal, however the prologue with the boundary definition must
|
34
|
+
# be always present.
|
36
35
|
#
|
37
36
|
# == Example Usage
|
38
37
|
#
|
39
38
|
# require "lib/doc_storage"
|
40
39
|
#
|
41
40
|
# # Create a new document with two parts
|
42
|
-
# document = DocStorage::
|
41
|
+
# document = DocStorage::MultipartDocument.new([
|
43
42
|
# DocStorage::SimpleDocument.new(
|
44
43
|
# {
|
45
44
|
# "Title" => "Finishing the documentation",
|
@@ -56,10 +55,8 @@ module DocStorage
|
|
56
55
|
# ),
|
57
56
|
# ])
|
58
57
|
#
|
59
|
-
# #
|
60
|
-
# document =
|
61
|
-
# DocStorage::MultiPartDocument.parse(f)
|
62
|
-
# end
|
58
|
+
# # Load from a file
|
59
|
+
# document = DocStorage::MultipartDocument.load_file("examples/multipart.txt")
|
63
60
|
#
|
64
61
|
# # Document manipulation
|
65
62
|
# document.parts << DocStorage::SimpleDocument.new(
|
@@ -71,30 +68,28 @@ module DocStorage
|
|
71
68
|
# )
|
72
69
|
#
|
73
70
|
# # Save the modified document
|
74
|
-
#
|
75
|
-
|
76
|
-
# end
|
77
|
-
class MultiPartDocument
|
71
|
+
# document.save_file("examples/multipart_modified.txt")
|
72
|
+
class MultipartDocument
|
78
73
|
# document parts (+Array+ of <tt>DocStorage::SimpleDocument</tt>)
|
79
74
|
attr_accessor :parts
|
80
75
|
|
81
76
|
class << self
|
82
77
|
private
|
83
|
-
def
|
84
|
-
prologue = SimpleDocument.
|
78
|
+
def load_from_io(io)
|
79
|
+
prologue = SimpleDocument.load(io, :detect)
|
85
80
|
boundary = prologue.headers["Boundary"]
|
86
81
|
|
87
82
|
parts = []
|
88
83
|
until io.eof?
|
89
|
-
parts << SimpleDocument.
|
84
|
+
parts << SimpleDocument.load(io, boundary)
|
90
85
|
end
|
91
86
|
|
92
|
-
|
87
|
+
MultipartDocument.new(parts)
|
93
88
|
end
|
94
89
|
|
95
90
|
public
|
96
|
-
#
|
97
|
-
# +
|
91
|
+
# Loads a multipart document from its serialized form and returns a new
|
92
|
+
# +MultipartDocument+ instance.
|
98
93
|
#
|
99
94
|
# The +source+ can be either an +IO+-like object or a +String+. In the
|
100
95
|
# latter case, it is assumed that the string contains a serialized
|
@@ -106,14 +101,25 @@ module DocStorage
|
|
106
101
|
# headers and body is parsed before the end of file) or if no "Boundary"
|
107
102
|
# header is found in the prologue.
|
108
103
|
#
|
109
|
-
# See the +
|
104
|
+
# See the +MultipartDocument+ class documentation for a detailed
|
110
105
|
# document format description.
|
111
|
-
def
|
112
|
-
|
106
|
+
def load(source)
|
107
|
+
load_from_io(source.is_a?(String) ? StringIO.new(source) : source)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Loads a multipart document from a file and returns a new
|
111
|
+
# +MultipartDocument+ instance. This method is just a thin wrapper
|
112
|
+
# around MultipartDocument#load -- see its documentation for description
|
113
|
+
# of the behavior and parameters of this method.
|
114
|
+
#
|
115
|
+
# See the +MultipartDocument+ class documentation for a detailed
|
116
|
+
# document format description.
|
117
|
+
def load_file(file)
|
118
|
+
File.open(file, "r") { |f| load(f) }
|
113
119
|
end
|
114
120
|
end
|
115
121
|
|
116
|
-
# Creates a new +
|
122
|
+
# Creates a new +MultipartDocument+ with given parts.
|
117
123
|
def initialize(parts)
|
118
124
|
@parts = parts
|
119
125
|
end
|
@@ -124,12 +130,15 @@ module DocStorage
|
|
124
130
|
other.instance_of?(self.class) && @parts == other.parts
|
125
131
|
end
|
126
132
|
|
127
|
-
# Returns string representation of this document. The result is in
|
128
|
-
# described in the +
|
133
|
+
# Returns string representation of this document. The result is in the
|
134
|
+
# format described in the +MultipartDocument+ class documentation.
|
135
|
+
#
|
136
|
+
# Raises +SyntaxError+ if any document header in any contained document has
|
137
|
+
# invalid name.
|
129
138
|
def to_s
|
130
139
|
# The boundary is just a random string. We do not check if the boudnary
|
131
140
|
# appears anywhere in the subdocuments, which may lead to malformed
|
132
|
-
# document.
|
141
|
+
# document. This is of course principially wrong, but the probability of
|
133
142
|
# collision is so small that it does not bother me much.
|
134
143
|
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
135
144
|
boundary = Array.new(64) { chars[rand(chars.length)] }.join("")
|
@@ -137,5 +146,23 @@ module DocStorage
|
|
137
146
|
SimpleDocument.new({"Boundary" => boundary}, "").to_s +
|
138
147
|
@parts.map { |part| "--#{boundary}\n#{part.to_s}" }.join("\n")
|
139
148
|
end
|
149
|
+
|
150
|
+
# Saves this document to an +IO+-like object. The result is in the format
|
151
|
+
# described in the +MultipartDocument+ class documentation.
|
152
|
+
#
|
153
|
+
# Raises +SyntaxError+ if any document header in any contained document has
|
154
|
+
# invalid name.
|
155
|
+
def save(io)
|
156
|
+
io.write(to_s)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Saves this document to a file. The result is in the format described in
|
160
|
+
# the +MultipartDocument+ class documentation.
|
161
|
+
#
|
162
|
+
# Raises +SyntaxError+ if any document header in any contained document has
|
163
|
+
# invalid name.
|
164
|
+
def save_file(file)
|
165
|
+
File.open(file, "w") { |f| save(f) }
|
166
|
+
end
|
140
167
|
end
|
141
168
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module DocStorage
|
2
2
|
# The +SimpleDocument+ class represents a simple RFC 822-like document,
|
3
|
-
# suitable for storing
|
3
|
+
# suitable for storing text associated with some metadata (e.g. a blog
|
4
4
|
# article with a title and a publication date). The +SimpleDocument+ class
|
5
5
|
# allows to create the document programatically, parse it from a file,
|
6
6
|
# manipulate its structure and save it to a file.
|
@@ -8,8 +8,7 @@ module DocStorage
|
|
8
8
|
# Each document consist of _headers_ and a _body_. Headers are a dictionary,
|
9
9
|
# mapping string names to string values. Body is a free-form text. The header
|
10
10
|
# names can contain only alphanumeric characters and a hyphen ("-") and they
|
11
|
-
# are case sensitive. The header values can contain any text
|
12
|
-
# begin with whitespace and does not contain a CR or LF character.
|
11
|
+
# are case sensitive. The header values can contain any text.
|
13
12
|
#
|
14
13
|
# == Document Format
|
15
14
|
#
|
@@ -23,11 +22,14 @@ module DocStorage
|
|
23
22
|
# Suspendisse metus sapien, consectetur vitae imperdiet vel, ornare a metus.
|
24
23
|
# In imperdiet euismod mi, nec volutpat lorem porta id.
|
25
24
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
25
|
+
# Headers are first, each on its own line. Header names are separated from
|
26
|
+
# values by a colon (":") and any amount of whitespace, trailing whitespace
|
27
|
+
# after values is ignored. Values containing special characters (especially
|
28
|
+
# newlines or leading/trailing whitepsace) must be enclosed in single or
|
29
|
+
# double quotes. Quoted values can contain usual C-like escape sequences (e.g.
|
30
|
+
# "\n", "\xFF", etc.). Duplicate headers are allowed with later value
|
31
|
+
# overwriting the earlier one. Other than that, the order of headers does not
|
32
|
+
# matter. The body is separated from headers by empty line.
|
31
33
|
#
|
32
34
|
# Documents without any headers are perfectly legal and so are documents with
|
33
35
|
# an empty body. However, the separating line must be always present. This
|
@@ -47,19 +49,15 @@ module DocStorage
|
|
47
49
|
# "We should finish the documentation ASAP."
|
48
50
|
# )
|
49
51
|
#
|
50
|
-
# #
|
51
|
-
# document =
|
52
|
-
# DocStorage::SimpleDocument.parse(f)
|
53
|
-
# end
|
52
|
+
# # Load from a file
|
53
|
+
# document = DocStorage::SimpleDocument.load_file("examples/simple.txt")
|
54
54
|
#
|
55
55
|
# # Document manipulation
|
56
56
|
# document.headers["Tags"] = "example"
|
57
57
|
# document.body += "Nulla mi dui, pellentesque et accumsan vitae, mattis et velit."
|
58
58
|
#
|
59
59
|
# # Save the modified document
|
60
|
-
#
|
61
|
-
# f.write(document)
|
62
|
-
# end
|
60
|
+
# document.save_file("examples/simple_modified.txt")
|
63
61
|
class SimpleDocument
|
64
62
|
# document headers (+Hash+)
|
65
63
|
attr_accessor :headers
|
@@ -68,6 +66,41 @@ module DocStorage
|
|
68
66
|
|
69
67
|
class << self
|
70
68
|
private
|
69
|
+
def parse_header_value(value)
|
70
|
+
case value[0..0]
|
71
|
+
when '"', "'"
|
72
|
+
quote = value[0..0]
|
73
|
+
if value[-1..-1] != quote
|
74
|
+
raise SyntaxError, "Unterminated header value: #{value.inspect}."
|
75
|
+
end
|
76
|
+
|
77
|
+
inner_text = value[1..-2]
|
78
|
+
if inner_text.gsub("\\" + quote, "").include?(quote)
|
79
|
+
raise SyntaxError, "Badly quoted header value: #{value.inspect}."
|
80
|
+
end
|
81
|
+
|
82
|
+
inner_text = inner_text.
|
83
|
+
gsub(/\\x([0-9a-fA-F]{2})/) { $1.to_i(16).chr }.
|
84
|
+
gsub(/\\([0-7]{3})/) { $1.to_i(8).chr }.
|
85
|
+
gsub("\\0", "\0").
|
86
|
+
gsub("\\a", "\a").
|
87
|
+
gsub("\\b", "\b").
|
88
|
+
gsub("\\t", "\t").
|
89
|
+
gsub("\\n", "\n").
|
90
|
+
gsub("\\v", "\v").
|
91
|
+
gsub("\\f", "\f").
|
92
|
+
gsub("\\r", "\r").
|
93
|
+
gsub("\\\"", "\"").
|
94
|
+
gsub("\\'", "'")
|
95
|
+
if inner_text !~ /^(\\\\|[^\\])*$/
|
96
|
+
raise SyntaxError, "Invalid escape sequence in header value: #{value.inspect}."
|
97
|
+
end
|
98
|
+
inner_text.gsub("\\\\", "\\")
|
99
|
+
else
|
100
|
+
value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
71
104
|
def parse_headers(io, detect_boundary)
|
72
105
|
result = {}
|
73
106
|
headers_terminated = false
|
@@ -75,13 +108,13 @@ module DocStorage
|
|
75
108
|
until io.eof?
|
76
109
|
line = io.readline
|
77
110
|
case line
|
78
|
-
when /^([a-zA-Z0-9-]+)
|
79
|
-
result[$1] = $2
|
111
|
+
when /^([a-zA-Z0-9-]+):(.*)\n$/
|
112
|
+
result[$1] = parse_header_value($2.strip)
|
80
113
|
when "\n"
|
81
114
|
headers_terminated = true
|
82
115
|
break
|
83
116
|
else
|
84
|
-
raise SyntaxError, "Invalid header:
|
117
|
+
raise SyntaxError, "Invalid header: #{line.sub(/\n$/, "").inspect}."
|
85
118
|
end
|
86
119
|
end
|
87
120
|
|
@@ -93,6 +126,10 @@ module DocStorage
|
|
93
126
|
result
|
94
127
|
end
|
95
128
|
|
129
|
+
def trim_last_char(s)
|
130
|
+
s[0..-2]
|
131
|
+
end
|
132
|
+
|
96
133
|
def parse_body(io, boundary)
|
97
134
|
if boundary
|
98
135
|
result = ""
|
@@ -102,18 +139,26 @@ module DocStorage
|
|
102
139
|
# Trim last newline from the body as it belongs to the boudnary
|
103
140
|
# logically. This behavior is implemented to allow bodies with
|
104
141
|
# no trailing newline).
|
105
|
-
return result
|
142
|
+
return trim_last_char(result)
|
106
143
|
end
|
107
144
|
|
108
145
|
result += line
|
109
146
|
end
|
110
|
-
|
147
|
+
|
148
|
+
# IO#readline always returns a newline at the end of a line, even
|
149
|
+
# when it physically wasn't there (which can happen at the end of a
|
150
|
+
# file). Note that only IO and its descendants behave this way (not
|
151
|
+
# StringIO, for example).
|
152
|
+
io.is_a?(IO) ? trim_last_char(result) : result
|
111
153
|
else
|
112
|
-
|
154
|
+
# IO#read always returns a newline at the end of the input, even
|
155
|
+
# when it physically wasn't there. Note that only IO and its
|
156
|
+
# descendants behave this way (not StringIO, for example).
|
157
|
+
io.is_a?(IO) ? trim_last_char(io.read) : io.read
|
113
158
|
end
|
114
159
|
end
|
115
160
|
|
116
|
-
def
|
161
|
+
def load_from_io(io, boundary)
|
117
162
|
headers = parse_headers(io, boundary == :detect)
|
118
163
|
boundary = headers["Boundary"] if boundary == :detect
|
119
164
|
body = parse_body(io, boundary)
|
@@ -122,7 +167,7 @@ module DocStorage
|
|
122
167
|
end
|
123
168
|
|
124
169
|
public
|
125
|
-
#
|
170
|
+
# Loads a simple document from its serialized form and returns a new
|
126
171
|
# +SimpleDocument+ instance.
|
127
172
|
#
|
128
173
|
# The +source+ can be either an +IO+-like object or a +String+. In the
|
@@ -145,23 +190,34 @@ module DocStorage
|
|
145
190
|
# read.
|
146
191
|
#
|
147
192
|
# The +boundary+ parameter is provided mainly for parsing parts of
|
148
|
-
# multipart documents (see the +
|
193
|
+
# multipart documents (see the +MultipartDocument+ class documentation)
|
149
194
|
# and usually should not be used.
|
150
195
|
#
|
151
196
|
# If any syntax error occurs, a +SyntaxError+ exception is raised. This
|
152
|
-
# can happen when an invalid header is encountered,
|
197
|
+
# can happen when an invalid header is encountered, headers are not
|
153
198
|
# terminated (no empty line separating headers and body is parsed before
|
154
199
|
# the end of file) or if no "Boundary" header is found when detecting a
|
155
200
|
# boundary.
|
156
201
|
#
|
157
202
|
# See the +SimpleDocument+ class documentation for a detailed document
|
158
203
|
# format description.
|
159
|
-
def
|
160
|
-
|
204
|
+
def load(source, boundary = nil)
|
205
|
+
load_from_io(
|
161
206
|
source.is_a?(String) ? StringIO.new(source) : source,
|
162
207
|
boundary
|
163
208
|
)
|
164
209
|
end
|
210
|
+
|
211
|
+
# Loads a simple document from a file and returns a new +SimpleDocument+
|
212
|
+
# instance. This method is just a thin wrapper around
|
213
|
+
# SimpleDocument#load -- see its documentation for description of the
|
214
|
+
# behavior and parameters of this method.
|
215
|
+
#
|
216
|
+
# See the +SimpleDocument+ class documentation for a detailed document
|
217
|
+
# format description.
|
218
|
+
def load_file(file, boundary = nil)
|
219
|
+
File.open(file, "r") { |f| load(f, boundary) }
|
220
|
+
end
|
165
221
|
end
|
166
222
|
|
167
223
|
# Creates a new +SimpleDocument+ with given headers and body.
|
@@ -177,13 +233,43 @@ module DocStorage
|
|
177
233
|
@body == other.body
|
178
234
|
end
|
179
235
|
|
180
|
-
# Returns string representation of this document. The result is in
|
181
|
-
# described in the +SimpleDocument+ class documentation.
|
236
|
+
# Returns string representation of this document. The result is in the
|
237
|
+
# format described in the +SimpleDocument+ class documentation.
|
238
|
+
#
|
239
|
+
# Raises +SyntaxError+ if any document header has invalid name.
|
182
240
|
def to_s
|
241
|
+
@headers.keys.each do |name|
|
242
|
+
if name !~ /\A[a-zA-Z0-9-]+\Z/
|
243
|
+
raise SyntaxError, "Invalid header name: #{name.inspect}."
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
183
247
|
serialized_headers = @headers.keys.sort.inject("") do |acc, key|
|
184
|
-
|
248
|
+
value_is_simple = @headers[key] !~ /\A\s+/ &&
|
249
|
+
@headers[key] !~ /\s+\Z/ &&
|
250
|
+
@headers[key] !~ /[\n\r]/
|
251
|
+
value = value_is_simple ? @headers[key] : @headers[key].inspect
|
252
|
+
|
253
|
+
acc + "#{key}: #{value}\n"
|
185
254
|
end
|
255
|
+
|
186
256
|
serialized_headers + "\n" + @body
|
187
257
|
end
|
258
|
+
|
259
|
+
# Saves this document to an +IO+-like object. The result is in the format
|
260
|
+
# described in the +SimpleDocument+ class documentation.
|
261
|
+
#
|
262
|
+
# Raises +SyntaxError+ if any document header has invalid name.
|
263
|
+
def save(io)
|
264
|
+
io.write(to_s)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Saves this document to a file. The result is in the format described in
|
268
|
+
# the +SimpleDocument+ class documentation.
|
269
|
+
#
|
270
|
+
# Raises +SyntaxError+ if any document header has invalid name.
|
271
|
+
def save_file(file)
|
272
|
+
File.open(file, "w") { |f| save(f) }
|
273
|
+
end
|
188
274
|
end
|
189
275
|
end
|
data/lib/doc_storage.rb
CHANGED
@@ -0,0 +1,215 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + "/../lib/doc_storage"
|
4
|
+
|
5
|
+
module DocStorage
|
6
|
+
describe MultipartDocument do
|
7
|
+
MULTIPART_FIXTURE_FILE = File.dirname(__FILE__) + "/fixtures/multipart.txt"
|
8
|
+
|
9
|
+
Spec::Matchers.define :load_as_multipart_document do |document|
|
10
|
+
match do |string|
|
11
|
+
MultipartDocument::load(string) == document
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
before :each do
|
16
|
+
@document = MultipartDocument.new([:part1, :part2])
|
17
|
+
|
18
|
+
@document_with_no_parts = MultipartDocument.new([])
|
19
|
+
@document_with_multiple_parts = MultipartDocument.new([
|
20
|
+
SimpleDocument.new({ "a" => "42", "b" => "43" }, "line1\nline2"),
|
21
|
+
SimpleDocument.new({ "c" => "44", "d" => "45" }, "line3\nline4"),
|
22
|
+
])
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "initialize" do
|
26
|
+
it "sets attributes correctly" do
|
27
|
+
@document.parts.should == [:part1, :part2]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "==" do
|
32
|
+
it "returns true when passed the same object" do
|
33
|
+
@document.should == @document
|
34
|
+
end
|
35
|
+
|
36
|
+
it "returns true when passed a MultipartDocument initialized with the same parameter" do
|
37
|
+
@document.should == MultipartDocument.new([:part1, :part2])
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns false when passed some random object" do
|
41
|
+
@document.should_not == Object.new
|
42
|
+
end
|
43
|
+
|
44
|
+
it "returns false when passed a subclass of MultipartDocument initialized with the same parameter" do
|
45
|
+
class SubclassedMultipartDocument < MultipartDocument
|
46
|
+
end
|
47
|
+
|
48
|
+
@document.should_not ==
|
49
|
+
SubclassedMultipartDocument.new([:part1, :part2])
|
50
|
+
end
|
51
|
+
|
52
|
+
it "returns false when passed a MultipartDocument initialized with different parameter" do
|
53
|
+
@document.should_not == MultipartDocument.new([:part3, :part4])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "load" do
|
58
|
+
it "loads document with no parts" do
|
59
|
+
"Boundary: =====\n\n".should load_as_multipart_document(
|
60
|
+
@document_with_no_parts
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "loads document with multiple parts" do
|
65
|
+
[
|
66
|
+
"Boundary: =====",
|
67
|
+
"",
|
68
|
+
"--=====",
|
69
|
+
"a: 42",
|
70
|
+
"b: 43",
|
71
|
+
"",
|
72
|
+
"line1",
|
73
|
+
"line2",
|
74
|
+
"--=====",
|
75
|
+
"c: 44",
|
76
|
+
"d: 45",
|
77
|
+
"",
|
78
|
+
"line3",
|
79
|
+
"line4",
|
80
|
+
].join("\n").should load_as_multipart_document(
|
81
|
+
@document_with_multiple_parts
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "does not load document with no Boundary: header" do
|
86
|
+
lambda {
|
87
|
+
MultipartDocument.load("\n\n")
|
88
|
+
}.should raise_error(SyntaxError, "No boundary defined.")
|
89
|
+
end
|
90
|
+
|
91
|
+
it "loads document from IO-like object" do
|
92
|
+
StringIO.open(
|
93
|
+
[
|
94
|
+
"Boundary: =====",
|
95
|
+
"",
|
96
|
+
"--=====",
|
97
|
+
"a: 42",
|
98
|
+
"b: 43",
|
99
|
+
"",
|
100
|
+
"line1",
|
101
|
+
"line2",
|
102
|
+
"--=====",
|
103
|
+
"c: 44",
|
104
|
+
"d: 45",
|
105
|
+
"",
|
106
|
+
"line3",
|
107
|
+
"line4",
|
108
|
+
].join("\n")
|
109
|
+
) do |io|
|
110
|
+
MultipartDocument.load(io).should == @document_with_multiple_parts
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "load_file" do
|
116
|
+
it "loads document" do
|
117
|
+
MultipartDocument.load_file(MULTIPART_FIXTURE_FILE).should ==
|
118
|
+
@document_with_multiple_parts
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "to_s" do
|
123
|
+
it "serializes document with no parts" do
|
124
|
+
srand 0
|
125
|
+
@document_with_no_parts.to_s.should ==
|
126
|
+
"Boundary: SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI\n\n"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "serializes document with multiple parts" do
|
130
|
+
srand 0
|
131
|
+
@document_with_multiple_parts.to_s.should == [
|
132
|
+
"Boundary: SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
133
|
+
"",
|
134
|
+
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
135
|
+
"a: 42",
|
136
|
+
"b: 43",
|
137
|
+
"",
|
138
|
+
"line1",
|
139
|
+
"line2",
|
140
|
+
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
141
|
+
"c: 44",
|
142
|
+
"d: 45",
|
143
|
+
"",
|
144
|
+
"line3",
|
145
|
+
"line4",
|
146
|
+
].join("\n")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "save" do
|
151
|
+
it "saves document" do
|
152
|
+
StringIO.open("", "w") do |io|
|
153
|
+
srand 0
|
154
|
+
@document_with_multiple_parts.save(io)
|
155
|
+
io.string.should == [
|
156
|
+
"Boundary: SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
157
|
+
"",
|
158
|
+
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
159
|
+
"a: 42",
|
160
|
+
"b: 43",
|
161
|
+
"",
|
162
|
+
"line1",
|
163
|
+
"line2",
|
164
|
+
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
165
|
+
"c: 44",
|
166
|
+
"d: 45",
|
167
|
+
"",
|
168
|
+
"line3",
|
169
|
+
"line4",
|
170
|
+
].join("\n")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "save_file" do
|
176
|
+
it "saves document" do
|
177
|
+
# The "ensure" blocks aren't really necessary -- the tempfile will be
|
178
|
+
# closed and unlinked upon its object destruction automatically. However
|
179
|
+
# I think that being explicit and deterministic doesn't hurt.
|
180
|
+
|
181
|
+
begin
|
182
|
+
tempfile = Tempfile.new("doc_storage")
|
183
|
+
tempfile.close
|
184
|
+
|
185
|
+
srand 0
|
186
|
+
@document_with_multiple_parts.save_file(tempfile.path)
|
187
|
+
|
188
|
+
tempfile.open
|
189
|
+
begin
|
190
|
+
tempfile.read.should == [
|
191
|
+
"Boundary: SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
192
|
+
"",
|
193
|
+
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
194
|
+
"a: 42",
|
195
|
+
"b: 43",
|
196
|
+
"",
|
197
|
+
"line1",
|
198
|
+
"line2",
|
199
|
+
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
200
|
+
"c: 44",
|
201
|
+
"d: 45",
|
202
|
+
"",
|
203
|
+
"line3",
|
204
|
+
"line4",
|
205
|
+
].join("\n")
|
206
|
+
ensure
|
207
|
+
tempfile.close
|
208
|
+
end
|
209
|
+
ensure
|
210
|
+
tempfile.unlink
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -1,26 +1,39 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
|
1
3
|
require File.dirname(__FILE__) + "/../lib/doc_storage"
|
2
4
|
|
3
5
|
module DocStorage
|
4
6
|
describe SimpleDocument do
|
5
|
-
|
7
|
+
SIMPLE_FIXTURE_FILE = File.dirname(__FILE__) + "/fixtures/simple.txt"
|
8
|
+
|
9
|
+
Spec::Matchers.define :load_as_document do |document|
|
6
10
|
match do |string|
|
7
|
-
SimpleDocument.
|
11
|
+
SimpleDocument.load(string) == document
|
8
12
|
end
|
9
13
|
end
|
10
14
|
|
11
15
|
before :each do
|
12
|
-
@document = SimpleDocument.new({"a" => 42, "b" => 43}, "body")
|
16
|
+
@document = SimpleDocument.new({ "a" => 42, "b" => 43 }, "body")
|
13
17
|
|
14
18
|
@document_without_headers_without_body = SimpleDocument.new({}, "")
|
15
19
|
@document_without_headers_with_body = SimpleDocument.new({}, "line1\nline2")
|
16
20
|
@document_with_headers_without_body = SimpleDocument.new(
|
17
|
-
{"a" => "42", "b" => "43"},
|
21
|
+
{ "a" => "42", "b" => "43" },
|
18
22
|
""
|
19
23
|
)
|
20
24
|
@document_with_headers_with_body = SimpleDocument.new(
|
21
|
-
{"a" => "42", "b" => "43"},
|
25
|
+
{ "a" => "42", "b" => "43" },
|
22
26
|
"line1\nline2"
|
23
27
|
)
|
28
|
+
|
29
|
+
@document_with_ugly_header = SimpleDocument.new(
|
30
|
+
{ "a" => "\xFF\377\0\a\b\t\n\v\f\r\"'\\\xFF\377\0\a\b\t\n\v\f\r\"'\\" },
|
31
|
+
""
|
32
|
+
)
|
33
|
+
@document_with_invalid_header = SimpleDocument.new(
|
34
|
+
{ "in\nvalid" => "42" },
|
35
|
+
""
|
36
|
+
)
|
24
37
|
end
|
25
38
|
|
26
39
|
describe "initialize" do
|
@@ -52,77 +65,159 @@ module DocStorage
|
|
52
65
|
end
|
53
66
|
|
54
67
|
it "returns false when passed a SimpleDocument initialized with different parameters" do
|
55
|
-
@document.should_not == SimpleDocument.new({"a" => 44, "b" => 45}, "body")
|
56
|
-
@document.should_not == SimpleDocument.new({"a" => 42, "b" => 43}, "nobody")
|
68
|
+
@document.should_not == SimpleDocument.new({ "a" => 44, "b" => 45 }, "body")
|
69
|
+
@document.should_not == SimpleDocument.new({ "a" => 42, "b" => 43 }, "nobody")
|
57
70
|
end
|
58
71
|
end
|
59
72
|
|
60
|
-
describe "
|
61
|
-
it "
|
62
|
-
"\n".should
|
73
|
+
describe "load" do
|
74
|
+
it "loads document with no headers and no body" do
|
75
|
+
"\n".should load_as_document(@document_without_headers_without_body)
|
63
76
|
end
|
64
77
|
|
65
|
-
it "
|
66
|
-
"\nline1\nline2".should
|
78
|
+
it "loads document with no headers and body" do
|
79
|
+
"\nline1\nline2".should load_as_document(
|
67
80
|
@document_without_headers_with_body
|
68
81
|
)
|
69
82
|
end
|
70
83
|
|
71
|
-
it "
|
72
|
-
"a: 42\nb: 43\n\n".should
|
84
|
+
it "loads document with headers and no body" do
|
85
|
+
"a: 42\nb: 43\n\n".should load_as_document(
|
73
86
|
@document_with_headers_without_body
|
74
87
|
)
|
75
88
|
end
|
76
89
|
|
77
|
-
it "
|
78
|
-
"a: 42\nb: 43\n\nline1\nline2".should
|
90
|
+
it "loads document with headers and body" do
|
91
|
+
"a: 42\nb: 43\n\nline1\nline2".should load_as_document(
|
79
92
|
@document_with_headers_with_body
|
80
93
|
)
|
81
94
|
end
|
82
95
|
|
83
|
-
it "
|
96
|
+
it "loads document with no whitespace after the colon in headers" do
|
97
|
+
"a:42\nb:43\n\n".should load_as_document(
|
98
|
+
@document_with_headers_without_body
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "loads document with multiple whitespace after the colon in headers" do
|
103
|
+
"a: \t 42\nb: \t 43\n\n".should load_as_document(
|
104
|
+
@document_with_headers_without_body
|
105
|
+
)
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
it "loads document with multiple whitespace after the value in headers" do
|
110
|
+
"a:42 \t \nb:43 \t \n\n".should load_as_document(
|
111
|
+
@document_with_headers_without_body
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "loads document with quoted header value" do
|
116
|
+
"a: \"42\"\nb: \"43\"\n\n".should load_as_document(
|
117
|
+
@document_with_headers_without_body
|
118
|
+
)
|
119
|
+
"a: '42'\nb: '43'\n\n".should load_as_document(
|
120
|
+
@document_with_headers_without_body
|
121
|
+
)
|
122
|
+
|
123
|
+
"a: \"\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\\"\n\n".should load_as_document(
|
124
|
+
@document_with_ugly_header
|
125
|
+
)
|
126
|
+
"a: '\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\\\xFF\\377\\0\\a\\b\\t\\n\\v\\f\\r\\\"\\'\\\\'\n\n".should load_as_document(
|
127
|
+
@document_with_ugly_header
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "does not load document with unterminated header value" do
|
132
|
+
lambda {
|
133
|
+
SimpleDocument.load("a: \"42\n\n")
|
134
|
+
}.should raise_error(SyntaxError, "Unterminated header value: \"\\\"42\".")
|
135
|
+
lambda {
|
136
|
+
SimpleDocument.load("a: '42\n\n")
|
137
|
+
}.should raise_error(SyntaxError, "Unterminated header value: \"'42\".")
|
138
|
+
end
|
139
|
+
|
140
|
+
it "does not load document with badly quoted header value" do
|
141
|
+
lambda {
|
142
|
+
SimpleDocument.load("a: \"4\"2\"\n\n")
|
143
|
+
}.should raise_error(SyntaxError, "Badly quoted header value: \"\\\"4\\\"2\\\"\".")
|
84
144
|
lambda {
|
85
|
-
SimpleDocument.
|
86
|
-
}.should raise_error(SyntaxError, "
|
145
|
+
SimpleDocument.load("a: '4'2'\n\n")
|
146
|
+
}.should raise_error(SyntaxError, "Badly quoted header value: \"'4'2'\".")
|
87
147
|
end
|
88
148
|
|
89
|
-
it "does not
|
149
|
+
it "does not load document with quoted header value containing invalid escape sequence" do
|
90
150
|
lambda {
|
91
|
-
SimpleDocument.
|
151
|
+
SimpleDocument.load("a: \"4\\z2\"\n\n")
|
152
|
+
}.should raise_error(SyntaxError, "Invalid escape sequence in header value: \"\\\"4\\\\z2\\\"\".")
|
153
|
+
lambda {
|
154
|
+
SimpleDocument.load("a: '4\\z2'\n\n")
|
155
|
+
}.should raise_error(SyntaxError, "Invalid escape sequence in header value: \"'4\\\\z2'\".")
|
156
|
+
end
|
157
|
+
|
158
|
+
it "does not load document with invalid headers" do
|
159
|
+
lambda {
|
160
|
+
SimpleDocument.load("bull\tshit\n")
|
161
|
+
}.should raise_error(SyntaxError, "Invalid header: \"bull\\tshit\".")
|
162
|
+
end
|
163
|
+
|
164
|
+
it "does not load document with unterminated headers" do
|
165
|
+
lambda {
|
166
|
+
SimpleDocument.load("a: 42\nb: 42\n")
|
92
167
|
}.should raise_error(SyntaxError, "Unterminated headers.")
|
93
168
|
end
|
94
169
|
|
95
|
-
it "
|
170
|
+
it "loads document from IO-like object" do
|
96
171
|
StringIO.open("a: 42\nb: 43\n\nline1\nline2") do |io|
|
97
|
-
SimpleDocument.
|
172
|
+
SimpleDocument.load(io).should == @document_with_headers_with_body
|
98
173
|
end
|
99
174
|
end
|
100
175
|
|
101
|
-
it "
|
102
|
-
SimpleDocument.
|
176
|
+
it "loads document when detecting a boundary" do
|
177
|
+
SimpleDocument.load(
|
103
178
|
"a: 42\nb: 43\nBoundary: =====\n\nline1\nline2\n--=====\nbullshit",
|
104
179
|
:detect
|
105
180
|
).should == SimpleDocument.new(
|
106
|
-
{"a" => "42", "b" => "43", "Boundary" => "====="},
|
181
|
+
{ "a" => "42", "b" => "43", "Boundary" => "=====" },
|
107
182
|
"line1\nline2"
|
108
183
|
)
|
109
184
|
end
|
110
185
|
|
111
|
-
it "does not
|
186
|
+
it "does not load document when detecting a boundary and no boundary defined" do
|
112
187
|
lambda {
|
113
|
-
SimpleDocument.
|
188
|
+
SimpleDocument.load(
|
114
189
|
"a: 42\nb: 43\n\nline1\nline2\n--=====\nbullshit",
|
115
190
|
:detect
|
116
191
|
)
|
117
192
|
}.should raise_error(SyntaxError, "No boundary defined.")
|
118
193
|
end
|
119
194
|
|
120
|
-
it "
|
121
|
-
SimpleDocument.
|
195
|
+
it "loads document when passed a boundary" do
|
196
|
+
SimpleDocument.load(
|
122
197
|
"a: 42\nb: 43\n\nline1\nline2\n--=====\nbullshit",
|
123
198
|
"====="
|
124
199
|
).should == @document_with_headers_with_body
|
125
200
|
end
|
201
|
+
|
202
|
+
it "works around the IO#readline bug" do
|
203
|
+
File.open(SIMPLE_FIXTURE_FILE, "r") do |f|
|
204
|
+
SimpleDocument.load(f).should == @document_with_headers_with_body
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
it "works around the IO#read bug when passed a boundary" do
|
209
|
+
File.open(SIMPLE_FIXTURE_FILE, "r") do |f|
|
210
|
+
SimpleDocument.load(f, "=====").should ==
|
211
|
+
@document_with_headers_with_body
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe "load_file" do
|
217
|
+
it "loads document" do
|
218
|
+
SimpleDocument.load_file(SIMPLE_FIXTURE_FILE).should ==
|
219
|
+
@document_with_headers_with_body
|
220
|
+
end
|
126
221
|
end
|
127
222
|
|
128
223
|
describe "to_s" do
|
@@ -142,6 +237,50 @@ module DocStorage
|
|
142
237
|
@document_with_headers_with_body.to_s.should ==
|
143
238
|
"a: 42\nb: 43\n\nline1\nline2"
|
144
239
|
end
|
240
|
+
|
241
|
+
it "serializes document with ugly header" do
|
242
|
+
@document_with_ugly_header.to_s.should ==
|
243
|
+
"a: \"\\377\\377\\000\\a\\b\\t\\n\\v\\f\\r\\\"'\\\\\\377\\377\\000\\a\\b\\t\\n\\v\\f\\r\\\"'\\\\\"\n\n"
|
244
|
+
end
|
245
|
+
|
246
|
+
it "does not serialize document with invalid header name" do
|
247
|
+
lambda {
|
248
|
+
@document_with_invalid_header.to_s
|
249
|
+
}.should raise_error(SyntaxError, "Invalid header name: \"in\\nvalid\".")
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
describe "save" do
|
254
|
+
it "saves document" do
|
255
|
+
StringIO.open("", "w") do |io|
|
256
|
+
@document_with_headers_with_body.save(io)
|
257
|
+
io.string.should == "a: 42\nb: 43\n\nline1\nline2"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
describe "save_file" do
|
263
|
+
it "saves document" do
|
264
|
+
# The "ensure" blocks aren't really necessary -- the tempfile will be
|
265
|
+
# closed and unlinked upon its object destruction automatically. However
|
266
|
+
# I think that being explicit and deterministic doesn't hurt.
|
267
|
+
|
268
|
+
begin
|
269
|
+
tempfile = Tempfile.new("doc_storage")
|
270
|
+
tempfile.close
|
271
|
+
|
272
|
+
@document_with_headers_with_body.save_file(tempfile.path)
|
273
|
+
|
274
|
+
tempfile.open
|
275
|
+
begin
|
276
|
+
tempfile.read.should == "a: 42\nb: 43\n\nline1\nline2"
|
277
|
+
ensure
|
278
|
+
tempfile.close
|
279
|
+
end
|
280
|
+
ensure
|
281
|
+
tempfile.unlink
|
282
|
+
end
|
283
|
+
end
|
145
284
|
end
|
146
285
|
end
|
147
286
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: doc_storage
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "0
|
4
|
+
version: "1.0"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Majda
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-02-14 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -27,17 +27,17 @@ files:
|
|
27
27
|
- LICENSE
|
28
28
|
- VERSION
|
29
29
|
- lib/doc_storage.rb
|
30
|
-
- lib/doc_storage/multi_part_document.rb
|
31
30
|
- lib/doc_storage/simple_document.rb
|
32
31
|
- lib/doc_storage/syntax_error.rb
|
32
|
+
- lib/doc_storage/multipart_document.rb
|
33
|
+
- spec/multipart_document_spec.rb
|
33
34
|
- spec/simple_document_spec.rb
|
34
|
-
- spec/multi_part_document_spec.rb
|
35
35
|
- examples/simple.txt
|
36
36
|
- examples/multipart.rb
|
37
37
|
- examples/simple.rb
|
38
38
|
- examples/multipart.txt
|
39
39
|
has_rdoc: true
|
40
|
-
homepage: http://
|
40
|
+
homepage: http://bitbucket.org/dmajda/doc_storage/
|
41
41
|
licenses: []
|
42
42
|
|
43
43
|
post_install_message:
|
@@ -1,139 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + "/../lib/doc_storage"
|
2
|
-
|
3
|
-
module DocStorage
|
4
|
-
describe MultiPartDocument do
|
5
|
-
Spec::Matchers.define :parse_as_multi_part_document do |document|
|
6
|
-
match do |string|
|
7
|
-
MultiPartDocument::parse(string) == document
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
before :each do
|
12
|
-
@document = MultiPartDocument.new([:part1, :part2])
|
13
|
-
|
14
|
-
@document_with_no_parts = MultiPartDocument.new([])
|
15
|
-
@document_with_multiple_parts = MultiPartDocument.new([
|
16
|
-
SimpleDocument.new({"a" => "42", "b" => "43"}, "line1\nline2"),
|
17
|
-
SimpleDocument.new({"c" => "44", "d" => "45"}, "line3\nline4"),
|
18
|
-
])
|
19
|
-
end
|
20
|
-
|
21
|
-
describe "initialize" do
|
22
|
-
it "sets attributes correctly" do
|
23
|
-
@document.parts.should == [:part1, :part2]
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
describe "==" do
|
28
|
-
it "returns true when passed the same object" do
|
29
|
-
@document.should == @document
|
30
|
-
end
|
31
|
-
|
32
|
-
it "returns true when passed a MultiPartDocument initialized with the same parameter" do
|
33
|
-
@document.should == MultiPartDocument.new([:part1, :part2])
|
34
|
-
end
|
35
|
-
|
36
|
-
it "returns false when passed some random object" do
|
37
|
-
@document.should_not == Object.new
|
38
|
-
end
|
39
|
-
|
40
|
-
it "returns false when passed a subclass of MultiPartDocument initialized with the same parameter" do
|
41
|
-
class SubclassedMultiPartDocument < MultiPartDocument
|
42
|
-
end
|
43
|
-
|
44
|
-
@document.should_not ==
|
45
|
-
SubclassedMultiPartDocument.new([:part1, :part2])
|
46
|
-
end
|
47
|
-
|
48
|
-
it "returns false when passed a MultiPartDocument initialized with different parameter" do
|
49
|
-
@document.should_not == MultiPartDocument.new([:part3, :part4])
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
describe "parse" do
|
54
|
-
it "parses document with no parts" do
|
55
|
-
"Boundary: =====\n\n".should parse_as_multi_part_document(
|
56
|
-
@document_with_no_parts
|
57
|
-
)
|
58
|
-
end
|
59
|
-
|
60
|
-
it "parses document with multiple parts" do
|
61
|
-
[
|
62
|
-
"Boundary: =====",
|
63
|
-
"",
|
64
|
-
"--=====",
|
65
|
-
"a: 42",
|
66
|
-
"b: 43",
|
67
|
-
"",
|
68
|
-
"line1",
|
69
|
-
"line2",
|
70
|
-
"--=====",
|
71
|
-
"c: 44",
|
72
|
-
"d: 45",
|
73
|
-
"",
|
74
|
-
"line3",
|
75
|
-
"line4",
|
76
|
-
].join("\n").should parse_as_multi_part_document(
|
77
|
-
@document_with_multiple_parts
|
78
|
-
)
|
79
|
-
end
|
80
|
-
|
81
|
-
it "does not parse document with no Boundary: header" do
|
82
|
-
lambda {
|
83
|
-
MultiPartDocument.parse("\n\n")
|
84
|
-
}.should raise_error(SyntaxError, "No boundary defined.")
|
85
|
-
end
|
86
|
-
|
87
|
-
it "parses document from IO-like object" do
|
88
|
-
StringIO.open(
|
89
|
-
[
|
90
|
-
"Boundary: =====",
|
91
|
-
"",
|
92
|
-
"--=====",
|
93
|
-
"a: 42",
|
94
|
-
"b: 43",
|
95
|
-
"",
|
96
|
-
"line1",
|
97
|
-
"line2",
|
98
|
-
"--=====",
|
99
|
-
"c: 44",
|
100
|
-
"d: 45",
|
101
|
-
"",
|
102
|
-
"line3",
|
103
|
-
"line4",
|
104
|
-
].join("\n")
|
105
|
-
) do |io|
|
106
|
-
MultiPartDocument.parse(io).should == @document_with_multiple_parts
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
describe "to_s" do
|
112
|
-
it "serializes document with no parts" do
|
113
|
-
srand 0
|
114
|
-
@document_with_no_parts.to_s.should ==
|
115
|
-
"Boundary: SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI\n\n"
|
116
|
-
end
|
117
|
-
|
118
|
-
it "serializes document with multiple parts" do
|
119
|
-
srand 0
|
120
|
-
@document_with_multiple_parts.to_s.should == [
|
121
|
-
"Boundary: SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
122
|
-
"",
|
123
|
-
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
124
|
-
"a: 42",
|
125
|
-
"b: 43",
|
126
|
-
"",
|
127
|
-
"line1",
|
128
|
-
"line2",
|
129
|
-
"--SV1ad7dNjtvYKxgyym6bMNxUyrLznijuZqZfpVasJyXZDttoNGbj5GFk0xJlY3CI",
|
130
|
-
"c: 44",
|
131
|
-
"d: 45",
|
132
|
-
"",
|
133
|
-
"line3",
|
134
|
-
"line4",
|
135
|
-
].join("\n")
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|