doc_storage 0.9 → 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +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
|