doc_storage 0.9 → 1.0

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