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 CHANGED
@@ -1,21 +1,19 @@
1
1
  = DocStorage
2
2
 
3
- http://github.com/dmajda/doc_storage
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 a text
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 relational database.
7
+ application without a database.
8
8
 
9
- The library distinguishes between <em>simple documents</em> and <em>multipart
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
- == Document Format
11
+ The library distinguishes between <em>simple documents</em> and <em>multipart
12
+ documents</em>.
17
13
 
18
- A simple document looks like this:
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
- A multipart document looks like this:
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::MultiPartDocument</tt> classes for more formal format
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
- # Parse a file
68
- document = File.open("examples/simple.txt", "r") do |f|
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
- File.open("examples/simple_modified.txt", "w") do |f|
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::MultiPartDocument.new([
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
- # Parse a file
103
- document = File.open("examples/multipart.txt", "r") do |f|
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
- File.open("examples/multipart_modified.txt", "w") do |f|
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 (david@majda.cz[mailto:david@majda.cz], www.majda.cz).
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", "--format", "nested"]
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.9"
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://github.com/dmajda/doc_storage"
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.9
1
+ 1.0
@@ -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::MultiPartDocument.new([
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
- # Parse a file
24
- document = File.open("#{dir}/multipart.txt", "r") do |f|
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
- File.open("#{dir}/multipart_modified.txt", "w") do |f|
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
- # Parse a file
15
- document = File.open("#{dir}/simple.txt", "r") do |f|
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
- File.open("#{dir}/simple_modified.txt", "w") do |f|
25
- f.write(document)
26
- end
22
+ document.save_file("#{dir}/simple_modified.txt")
@@ -1,12 +1,11 @@
1
1
  module DocStorage
2
- # The +MultiPartDocument+ class represents a document consisting of several
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 a text associated with
6
- # some metadata (e.g. blog comments, each with an author and a publication
7
- # date). The +MultiPartDocument+ class allows to create the document
8
- # programatically, parse it from a file, manipulate its structure and save it
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 the _parts_ of the multipart document. Documents without any
34
- # parts are perfectly legal, however the prologue with the boundary definition
35
- # must be always present.
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::MultiPartDocument.new([
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
- # # Parse a file
60
- # document = File.open("examples/multipart.txt", "r") do |f|
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
- # File.open("examples/multipart_modified.txt", "w") do |f|
75
- # f.write(document)
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 parse_from_io(io)
84
- prologue = SimpleDocument.parse(io, :detect)
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.parse(io, boundary)
84
+ parts << SimpleDocument.load(io, boundary)
90
85
  end
91
86
 
92
- MultiPartDocument.new(parts)
87
+ MultipartDocument.new(parts)
93
88
  end
94
89
 
95
90
  public
96
- # Parses a multipart document from its serialized form and returns a new
97
- # +MultiPartDocument+ instance.
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 +MultiPartDocument+ class documentation for a detailed
104
+ # See the +MultipartDocument+ class documentation for a detailed
110
105
  # document format description.
111
- def parse(source)
112
- parse_from_io(source.is_a?(String) ? StringIO.new(source) : source)
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 +MultiPartDocument+ with given parts.
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 format
128
- # described in the +MultiPartDocument+ class documentation.
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. This is of course principially wrong, but the probability of
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 a text associated with some metadata (e.g. a blog
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 that does not
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
- # The headers are first, each on its own line. The header names are separated
27
- # from values by a colon (":") and any amount of whitespace. Duplicate headers
28
- # are allowed with later value overwriting the earlier one. Otherwise, the
29
- # order of the headers does not matter. The body is separated from the headers
30
- # by an empty line.
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
- # # Parse a file
51
- # document = File.open("examples/simple.txt", "r") do |f|
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
- # File.open("examples/simple_modified.txt", "w") do |f|
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-]+):\s(.*)\n$/
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: \"#{line.strip}\"."
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[0..-2]
142
+ return trim_last_char(result)
106
143
  end
107
144
 
108
145
  result += line
109
146
  end
110
- result
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
- io.read
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 parse_from_io(io, boundary)
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
- # Parses a simple document from its serialized form and returns a new
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 +MultiPartDocument+ class documentation)
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, the headers are not
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 parse(source, boundary = nil)
160
- parse_from_io(
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 format
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
- acc + "#{key}: #{@headers[key]}\n"
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
@@ -1,3 +1,3 @@
1
1
  require File.dirname(__FILE__) + "/doc_storage/syntax_error"
2
2
  require File.dirname(__FILE__) + "/doc_storage/simple_document"
3
- require File.dirname(__FILE__) + "/doc_storage/multi_part_document"
3
+ require File.dirname(__FILE__) + "/doc_storage/multipart_document"
@@ -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
- Spec::Matchers.define :parse_as_document do |document|
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.parse(string) == document
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 "parse" do
61
- it "parses document with no headers and no body" do
62
- "\n".should parse_as_document(@document_without_headers_without_body)
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 "parses document with no headers and body" do
66
- "\nline1\nline2".should parse_as_document(
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 "parses document with headers and no body" do
72
- "a: 42\nb: 43\n\n".should parse_as_document(
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 "parses document with headers and body" do
78
- "a: 42\nb: 43\n\nline1\nline2".should parse_as_document(
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 "does not parse document with invalid headers" do
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.parse("bullshit")
86
- }.should raise_error(SyntaxError, "Invalid header: \"bullshit\".")
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 parse document with unterminated headers" do
149
+ it "does not load document with quoted header value containing invalid escape sequence" do
90
150
  lambda {
91
- SimpleDocument.parse("a: 42\nb: 42\n")
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 "parses document from IO-like object" do
170
+ it "loads document from IO-like object" do
96
171
  StringIO.open("a: 42\nb: 43\n\nline1\nline2") do |io|
97
- SimpleDocument.parse(io).should == @document_with_headers_with_body
172
+ SimpleDocument.load(io).should == @document_with_headers_with_body
98
173
  end
99
174
  end
100
175
 
101
- it "parses document when detecting a boundary" do
102
- SimpleDocument.parse(
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 parse document when detecting a boundary and no boundary defined" do
186
+ it "does not load document when detecting a boundary and no boundary defined" do
112
187
  lambda {
113
- SimpleDocument.parse(
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 "parses document when passed a boundary" do
121
- SimpleDocument.parse(
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.9"
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: 2009-11-19 00:00:00 +01:00
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://github.com/dmajda/doc_storage
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