xml-write-stream 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a14bdeb8c25d97029a82e7afbe8bd8cd0247593a
4
+ data.tar.gz: 37b5e95c34ad08e90db5a5dd07c07be5ac2db843
5
+ SHA512:
6
+ metadata.gz: f46bd1fe8b2489370cea9299e3ce94ee08069549bdb3e3a12c9db94b0d47e7222bd67b8b9daf2e4e6e8b8e897623a552defb79b7741ef25c3521095172129205
7
+ data.tar.gz: 5fa0ad54a777b0db21a3b1fa23887950cded7b05649812511a3c20f1e7108f2802c6d102ec4272af7ceb7775f3458bc1fefed8423a44821f6e890099603ac969
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry', '~> 0.9.0'
7
+ gem 'pry-nav'
8
+ gem 'rake'
9
+ end
10
+
11
+ group :test do
12
+ gem 'rspec'
13
+ gem 'rr'
14
+ end
@@ -0,0 +1,3 @@
1
+ == 1.0.0
2
+
3
+ * Birthday!
@@ -0,0 +1,110 @@
1
+ xml-write-stream
2
+ =================
3
+
4
+ [![Build Status](https://travis-ci.org/camertron/xml-write-stream.svg?branch=master)](http://travis-ci.org/camertron/xml-write-stream)
5
+
6
+ An easy, streaming way to generate XML.
7
+
8
+ ## Installation
9
+
10
+ `gem install xml-write-stream`
11
+
12
+ ## Usage
13
+
14
+ ```ruby
15
+ require 'xml-write-stream'
16
+ ```
17
+
18
+ ### Examples for the Impatient
19
+
20
+ There are two types of XML write stream: one that uses blocks and `yield` to write tags, and one that's purely stateful. Here are two examples that produce the same output:
21
+
22
+ Yielding:
23
+
24
+ ```ruby
25
+ stream = StringIO.new
26
+ XmlWriteStream.from_stream(stream) do |writer|
27
+ writer.open_tag('foo', bar: 'baz') do |foo_writer|
28
+ foo_writer.open_tag('no-text')
29
+ foo_writer.write_text('blarg')
30
+ end
31
+ end
32
+ ```
33
+
34
+ Stateful:
35
+
36
+ ```ruby
37
+ stream = StringIO.new
38
+ writer = XmlWriteStream.from_stream(stream)
39
+ writer.open_tag('foo', bar: 'baz')
40
+ writer.open_tag('no-text')
41
+ writer.close_tag
42
+ writer.write_text('blarg')
43
+ writer.close # automatically adds closing tags for all unclosed tags
44
+ ```
45
+
46
+ Output:
47
+
48
+ ```ruby
49
+ stream.string # => <foo bar="baz"><no-text/>blarg</foo>
50
+ ```
51
+
52
+ ### Yielding Writers
53
+
54
+ As far as yielding writers go, the example above contains everything you need. The stream will be automatically closed when the outermost block terminates.
55
+
56
+ ### Stateful Writers
57
+
58
+ Stateful writers have a number of additional methods:
59
+
60
+ ```ruby
61
+ stream = StringIO.new
62
+ writer = XmlWriteStream.from_stream(stream)
63
+ writer.open_tag('foo')
64
+
65
+ writer.eos? # => false, the stream is open and the outermost tag hasn't been closed yet
66
+
67
+ writer.open_tag # explicitly close the current tag
68
+ writer.eos? # => true, the outermost tag has been closed
69
+
70
+ writer.open_tag('foo') # => raises XmlWriteStream::EndOfStreamError
71
+
72
+ writer.closed? # => false, the stream is still open
73
+ writer.close # close the stream
74
+ writer.closed? # => true, the stream has been closed
75
+ ```
76
+
77
+ ### Writing to a File
78
+
79
+ XmlWriteStream also supports streaming to a file via the `open` method:
80
+
81
+ Yielding:
82
+
83
+ ```ruby
84
+ XmlWriteStream.open('path/to/file.xml') do |writer|
85
+ writer.open_tag('foo') do |foo_writer|
86
+ ...
87
+ end
88
+ end
89
+ ```
90
+
91
+ Stateful:
92
+
93
+ ```ruby
94
+ writer = XmlWriteStream.open('path/to/file.xml')
95
+ writer.open_tag('foo')
96
+ ...
97
+ writer.close
98
+ ```
99
+
100
+ ## Requirements
101
+
102
+ No external requirements.
103
+
104
+ ## Running Tests
105
+
106
+ `bundle exec rake` should do the trick. Alternatively you can run `bundle exec rspec`, which does the same thing.
107
+
108
+ ## Authors
109
+
110
+ * Cameron C. Dutro: http://github.com/camertron
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
4
+
5
+ require 'bundler'
6
+ require 'rspec/core/rake_task'
7
+ require 'rubygems/package_task'
8
+
9
+ require './lib/xml-write-stream'
10
+
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ task :default => :spec
14
+
15
+ desc 'Run specs'
16
+ RSpec::Core::RakeTask.new do |t|
17
+ t.pattern = './spec/**/*_spec.rb'
18
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'xml-write-stream/base'
4
+ require 'xml-write-stream/yielding_writer'
5
+ require 'xml-write-stream/stateful_writer'
6
+
7
+ class XmlWriteStream
8
+ class InvalidAttributeKeyError < StandardError; end
9
+ class InvalidTagNameError < StandardError; end
10
+ class EndOfStreamError < StandardError; end
11
+ class NoTopLevelTagError < StandardError; end
12
+ class InvalidHeaderPositionError < StandardError; end
13
+
14
+ DEFAULT_ENCODING = Encoding::UTF_8
15
+
16
+ class << self
17
+ def from_stream(stream, encoding = DEFAULT_ENCODING)
18
+ stream.set_encoding(encoding)
19
+
20
+ if block_given?
21
+ yield writer = YieldingWriter.new(stream)
22
+ writer.close
23
+ else
24
+ StatefulWriter.new(stream)
25
+ end
26
+ end
27
+
28
+ def open(file, encoding = DEFAULT_ENCODING)
29
+ handle = File.open(file, 'w')
30
+ handle.set_encoding(encoding)
31
+
32
+ if block_given?
33
+ yield writer = YieldingWriter.new(handle)
34
+ writer.close
35
+ else
36
+ StatefulWriter.new(handle)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: UTF-8
2
+
3
+ class XmlWriteStream
4
+ class Base
5
+ DEFAULT_INDENT = 4
6
+
7
+ # these are probably fairly incorrect, but good enough for now
8
+ TAG_NAME_REGEX = /\A[a-zA-Z:_][\w.\-_:]*/
9
+ ATTRIBUTE_KEY_REGEX = /\A[a-zA-Z_][\w\-_]*/
10
+
11
+ TEXT_ESCAPE_CHARS = /["'<>&]/
12
+ ATTRIBUTE_ESCAPE_CHARS = /["'<>&\n\r\t]/
13
+
14
+ TEXT_ESCAPE_HASH = {
15
+ '"' => '&quot;',
16
+ "'" => '&apos;',
17
+ '<' => '&lt;',
18
+ '>' => '&gt;',
19
+ '&' => '&amp;'
20
+ }
21
+
22
+ ATTRIBUTE_ESCAPE_HASH = TEXT_ESCAPE_HASH.merge({
23
+ "\n" => '&#xA;',
24
+ "\r" => '&#xD;',
25
+ "\t" => '&#x9;'
26
+ })
27
+
28
+ DEFAULT_HEADER_ATTRIBUTES = {
29
+ version: '1.0',
30
+ encoding: 'utf-8'
31
+ }
32
+
33
+ def write_text(text, options = {})
34
+ escape = options.fetch(:escape, true)
35
+ stream.write(indent_spaces)
36
+
37
+ stream.write(
38
+ escape ? escape_text(text) : text
39
+ )
40
+
41
+ write_newline
42
+ end
43
+
44
+ def write_header(attributes = {})
45
+ stream.write('<?xml ')
46
+
47
+ write_attributes(
48
+ DEFAULT_HEADER_ATTRIBUTES.merge(attributes)
49
+ )
50
+
51
+ stream.write('?>')
52
+ write_newline
53
+ end
54
+
55
+ protected
56
+
57
+ def check_tag_name(tag_name)
58
+ unless tag_name =~ TAG_NAME_REGEX
59
+ raise InvalidTagNameError, "'#{tag_name}' is not a valid tag"
60
+ end
61
+ end
62
+
63
+ def check_attributes(attributes)
64
+ attributes.each_pair do |key, _|
65
+ unless key =~ ATTRIBUTE_KEY_REGEX
66
+ raise InvalidAttributeKeyError,
67
+ "'#{key}' is not a valid attribute key"
68
+ end
69
+ end
70
+ end
71
+
72
+ def write_open_tag(tag_name, attributes)
73
+ stream.write(indent_spaces)
74
+ stream.write("<#{tag_name}")
75
+
76
+ if attributes.size > 0
77
+ stream.write(' ')
78
+ write_attributes(attributes)
79
+ end
80
+
81
+ stream.write('>')
82
+ end
83
+
84
+ def write_close_tag(tag_name)
85
+ stream.write(indent_spaces)
86
+ stream.write("</#{tag_name}>")
87
+ end
88
+
89
+ def write_attributes(attributes)
90
+ attributes.each_pair.with_index do |(key, val), idx|
91
+ if idx > 0
92
+ stream.write(' ')
93
+ end
94
+
95
+ stream.write("#{key}=\"#{escape_attribute(val)}\"")
96
+ end
97
+ end
98
+
99
+ def write_newline
100
+ stream.write("\n")
101
+ end
102
+
103
+ def escape_attribute(attribute)
104
+ attribute.gsub(ATTRIBUTE_ESCAPE_CHARS, ATTRIBUTE_ESCAPE_HASH)
105
+ end
106
+
107
+ def escape_text(text)
108
+ text.gsub(TEXT_ESCAPE_CHARS, TEXT_ESCAPE_HASH)
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,87 @@
1
+ # encoding: UTF-8
2
+
3
+ class XmlWriteStream
4
+ class StatefulWriter < Base
5
+ attr_reader :stream, :stack, :closed, :indent, :index
6
+ alias :closed? :closed
7
+
8
+ def initialize(stream, options = {})
9
+ @stream = stream
10
+ @stack = []
11
+ @closed = false
12
+ @index = 0
13
+ @indent = options.fetch(:indent, Base::DEFAULT_INDENT)
14
+ end
15
+
16
+ def open_tag(tag_name, attributes = {})
17
+ check_eos
18
+ @index += 1
19
+
20
+ check_tag_name(tag_name)
21
+ check_attributes(attributes)
22
+ write_open_tag(tag_name, attributes)
23
+ write_newline
24
+
25
+ stack.push(tag_name)
26
+ end
27
+
28
+ def write_text(text, options = {})
29
+ check_eos
30
+
31
+ if stack.size == 0
32
+ raise NoTopLevelTagError
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ def write_header(attributes = {})
39
+ if stack.size > 0
40
+ raise InvalidHeaderPositionError,
41
+ 'header must be the first element written.'
42
+ end
43
+
44
+ super
45
+ end
46
+
47
+ def close_tag(options = {})
48
+ if in_tag?
49
+ tag_name = stack.pop
50
+ write_close_tag(tag_name)
51
+ write_newline
52
+ end
53
+ end
54
+
55
+ def flush
56
+ close_tag until stack.empty?
57
+ @closed = true
58
+ nil
59
+ end
60
+
61
+ def close
62
+ flush
63
+ stream.close
64
+ nil
65
+ end
66
+
67
+ def in_tag?
68
+ stack.size > 0 && !closed?
69
+ end
70
+
71
+ def eos?
72
+ (stack.size == 0 && index > 0) || closed?
73
+ end
74
+
75
+ protected
76
+
77
+ def indent_spaces
78
+ ' ' * (stack.size * indent)
79
+ end
80
+
81
+ def check_eos
82
+ if eos?
83
+ raise EndOfStreamError, 'end of stream.'
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ class XmlWriteStream
4
+ VERSION = "1.0.2"
5
+ end
@@ -0,0 +1,65 @@
1
+ # encoding: UTF-8
2
+
3
+ class XmlWriteStream
4
+ class YieldingWriter < Base
5
+ attr_reader :stream, :level, :indent
6
+
7
+ def initialize(stream, options = {})
8
+ @stream = stream
9
+ @level = 0
10
+ @indent = options.fetch(:indent, Base::DEFAULT_INDENT)
11
+ end
12
+
13
+ def open_tag(tag_name, attributes = {})
14
+ check_closed
15
+ check_tag_name(tag_name)
16
+ check_attributes(attributes)
17
+ write_open_tag(tag_name, attributes)
18
+ write_newline
19
+
20
+ @level += 1
21
+ yield self if block_given?
22
+ @level -= 1
23
+ write_close_tag(tag_name)
24
+ write_newline
25
+ end
26
+
27
+ def write_text(text, options = {})
28
+ check_closed
29
+
30
+ if level == 0
31
+ raise NoTopLevelTagError
32
+ end
33
+
34
+ super
35
+ end
36
+
37
+ def write_header(attributes = {})
38
+ if level > 0
39
+ raise InvalidHeaderPositionError,
40
+ 'header must be the first element written.'
41
+ end
42
+
43
+ super
44
+ end
45
+
46
+ def flush
47
+ end
48
+
49
+ def close
50
+ stream.close
51
+ end
52
+
53
+ protected
54
+
55
+ def check_closed
56
+ if stream.closed?
57
+ raise EndOfStreamError, 'end of stream.'
58
+ end
59
+ end
60
+
61
+ def indent_spaces
62
+ ' ' * (level * indent)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rspec'
4
+ require 'xml-write-stream'
5
+ require 'pry-nav'
6
+
7
+ RSpec.configure do |config|
8
+ end
@@ -0,0 +1,192 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe XmlWriteStream::YieldingWriter do
6
+ let(:stream) do
7
+ StringIO.new.tap do |io|
8
+ io.set_encoding(Encoding::UTF_8)
9
+ end
10
+ end
11
+
12
+ let(:stream_writer) do
13
+ XmlWriteStream::StatefulWriter.new(stream)
14
+ end
15
+
16
+ def utf8(str)
17
+ str.encode(Encoding::UTF_8)
18
+ end
19
+
20
+ describe '#write_header' do
21
+ it 'writes the header with default attributes' do
22
+ stream_writer.write_header
23
+ stream_writer.close
24
+
25
+ expect(stream.string).to eq(
26
+ utf8("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
27
+ )
28
+ end
29
+
30
+ it 'allows header attributes to be overwritten' do
31
+ stream_writer.write_header(version: '2.0')
32
+ stream_writer.close
33
+
34
+ expect(stream.string).to eq(
35
+ utf8("<?xml version=\"2.0\" encoding=\"utf-8\"?>\n")
36
+ )
37
+ end
38
+
39
+ it 'raises an error if tags have already been written' do
40
+ stream_writer.open_tag('foo')
41
+
42
+ expect do
43
+ stream_writer.write_header
44
+ end.to raise_error(XmlWriteStream::InvalidHeaderPositionError)
45
+ end
46
+ end
47
+
48
+ describe '#open_tag' do
49
+ it 'writes an opening tag' do
50
+ stream_writer.open_tag('maytag')
51
+ stream_writer.close
52
+
53
+ expect(stream.string).to eq(
54
+ utf8("<maytag>\n</maytag>\n")
55
+ )
56
+ end
57
+
58
+ it 'writes an opening tag with attributes' do
59
+ stream_writer.open_tag('maytag', type: 'washing_machine')
60
+ stream_writer.close
61
+
62
+ expect(stream.string).to eq(
63
+ utf8("<maytag type=\"washing_machine\">\n</maytag>\n")
64
+ )
65
+ end
66
+
67
+ it 'raises an error if one of the attribute keys is invalid' do
68
+ expect do
69
+ stream_writer.open_tag('maytag', '0foo' => '')
70
+ end.to raise_error(XmlWriteStream::InvalidAttributeKeyError)
71
+ end
72
+
73
+ it 'raises an error if the tag name is invalid' do
74
+ expect do
75
+ stream_writer.open_tag('9foo')
76
+ end.to raise_error(XmlWriteStream::InvalidTagNameError)
77
+ end
78
+
79
+ it 'allows digits and colons in the tag name' do
80
+ stream_writer.open_tag('foo9')
81
+ stream_writer.open_tag('bar:baz')
82
+ stream_writer.close
83
+
84
+ expect(stream.string).to eq(
85
+ utf8("<foo9>\n <bar:baz>\n </bar:baz>\n</foo9>\n")
86
+ )
87
+ end
88
+
89
+ it 'raises an error if the stream is already closed' do
90
+ stream_writer.close
91
+
92
+ expect do
93
+ stream_writer.open_tag('foo')
94
+ end.to raise_error(XmlWriteStream::EndOfStreamError)
95
+ end
96
+ end
97
+
98
+ describe '#close_tag' do
99
+ it 'closes the currently open tag' do
100
+ stream_writer.open_tag('maytag')
101
+ stream_writer.close_tag
102
+
103
+ expect(stream.string).to eq(
104
+ utf8("<maytag>\n</maytag>\n")
105
+ )
106
+ end
107
+ end
108
+
109
+ describe '#write_text' do
110
+ it 'writes escaped text by default' do
111
+ stream_writer.open_tag('places')
112
+ stream_writer.write_text("Alaska & Hawai'i")
113
+ stream_writer.close
114
+
115
+ expect(stream.string).to eq(
116
+ utf8("<places>\n Alaska &amp; Hawai&apos;i\n</places>\n")
117
+ )
118
+ end
119
+
120
+ it 'writes raw text if asked not to escape' do
121
+ stream_writer.open_tag('places')
122
+ stream_writer.write_text("Alaska & Hawai'i", escape: false)
123
+ stream_writer.close
124
+
125
+ expect(stream.string).to eq(
126
+ utf8("<places>\n Alaska & Hawai'i\n</places>\n")
127
+ )
128
+ end
129
+
130
+ it 'raises an error if no tag has been written yet' do
131
+ expect do
132
+ stream_writer.write_text('foo')
133
+ end.to raise_error(XmlWriteStream::NoTopLevelTagError)
134
+ end
135
+
136
+ it 'raises an error if the stream is already closed' do
137
+ stream_writer.close
138
+
139
+ expect do
140
+ stream_writer.write_text('foo')
141
+ end.to raise_error(XmlWriteStream::EndOfStreamError)
142
+ end
143
+ end
144
+
145
+ describe '#flush' do
146
+ it 'closes all open tags' do
147
+ stream_writer.open_tag('foo')
148
+ stream_writer.open_tag('bar')
149
+ stream_writer.open_tag('baz')
150
+ stream_writer.flush
151
+
152
+ expect(stream.string).to eq(
153
+ utf8("<foo>\n <bar>\n <baz>\n </baz>\n </bar>\n</foo>\n")
154
+ )
155
+
156
+ expect(stream).to_not be_closed
157
+ expect(stream_writer).to be_eos
158
+ end
159
+ end
160
+
161
+ describe '#close' do
162
+ it 'closes all open tags and closes the stream' do
163
+ stream_writer.open_tag('foo')
164
+ stream_writer.open_tag('bar')
165
+ stream_writer.open_tag('baz')
166
+ stream_writer.close
167
+
168
+ expect(stream.string).to eq(
169
+ utf8("<foo>\n <bar>\n <baz>\n </baz>\n </bar>\n</foo>\n")
170
+ )
171
+
172
+ expect(stream).to be_closed
173
+ expect(stream_writer).to be_eos
174
+ end
175
+ end
176
+
177
+ describe '#in_tag?' do
178
+ it 'returns true if currently writing a tag, false otherwise' do
179
+ expect(stream_writer).to_not be_in_tag
180
+ stream_writer.open_tag('foo')
181
+ expect(stream_writer).to be_in_tag
182
+ end
183
+ end
184
+
185
+ describe '#eos?' do
186
+ it 'returns true if the stream is closed, false if it is still open' do
187
+ expect(stream_writer).to_not be_eos
188
+ stream_writer.close
189
+ expect(stream_writer).to be_eos
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,77 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+ require 'tempfile'
5
+
6
+ describe XmlWriteStream do
7
+ let(:yielding_writer) { XmlWriteStream::YieldingWriter }
8
+ let(:stateful_writer) { XmlWriteStream::StatefulWriter }
9
+ let(:stream_writer) { XmlWriteStream }
10
+ let(:tempfile) { Tempfile.new('temp') }
11
+ let(:stream) { StringIO.new }
12
+
13
+ describe '#from_stream' do
14
+ it 'yields a yielding stream if given a block' do
15
+ stream_writer.from_stream(stream) do |writer|
16
+ expect(writer).to be_a(yielding_writer)
17
+ expect(writer.stream).to equal(stream)
18
+ end
19
+ end
20
+
21
+ it 'returns a stateful writer if not given a block' do
22
+ writer = stream_writer.from_stream(stream)
23
+ expect(writer).to be_a(stateful_writer)
24
+ expect(writer.stream).to equal(stream)
25
+ end
26
+
27
+ it 'supports specifying a different encoding' do
28
+ stream_writer.from_stream(stream, Encoding::UTF_16BE) do |writer|
29
+ writer.open_tag('foo') do |tag_writer|
30
+ tag_writer.write_text('bar')
31
+ end
32
+ end
33
+
34
+ expect(stream.string.bytes.to_a).to_not eq(
35
+ "<foo>\n bar\n</foo>\n".bytes.to_a
36
+ )
37
+
38
+ expect(stream.string.encode(Encoding::UTF_8).bytes.to_a).to eq(
39
+ "<foo>\n bar\n</foo>\n".bytes.to_a
40
+ )
41
+ end
42
+ end
43
+
44
+ describe '#open' do
45
+ it 'opens a file and yields a yielding stream if given a block' do
46
+ stream_writer.open(tempfile) do |writer|
47
+ expect(writer).to be_a(yielding_writer)
48
+ expect(writer.stream.path).to eq(tempfile.path)
49
+ end
50
+ end
51
+
52
+ it 'opens a file and returns a stateful writer if not given a block' do
53
+ writer = stream_writer.open(tempfile)
54
+ expect(writer).to be_a(stateful_writer)
55
+ expect(writer.stream.path).to eq(tempfile.path)
56
+ end
57
+
58
+ it 'supports specifying a different encoding' do
59
+ stream_writer.open(tempfile, Encoding::UTF_16BE) do |writer|
60
+ writer.open_tag('foo') do |tag_writer|
61
+ tag_writer.write_text('bar')
62
+ end
63
+ end
64
+
65
+ written = tempfile.read
66
+ written.force_encoding(Encoding::UTF_16BE)
67
+
68
+ expect(written.bytes.to_a).to_not eq(
69
+ "<foo>\n bar\n</foo>\n".bytes.to_a
70
+ )
71
+
72
+ expect(written.encode(Encoding::UTF_8).bytes.to_a).to eq(
73
+ "<foo>\n bar\n</foo>\n".bytes.to_a
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,147 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe XmlWriteStream::YieldingWriter do
6
+ let(:stream) do
7
+ StringIO.new.tap do |io|
8
+ io.set_encoding(Encoding::UTF_8)
9
+ end
10
+ end
11
+
12
+ let(:stream_writer) do
13
+ XmlWriteStream::YieldingWriter.new(stream)
14
+ end
15
+
16
+ def utf8(str)
17
+ str.encode(Encoding::UTF_8)
18
+ end
19
+
20
+ describe '#write_header' do
21
+ it 'writes the header with default attributes' do
22
+ stream_writer.write_header
23
+ stream_writer.close
24
+
25
+ expect(stream.string).to eq(
26
+ utf8("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
27
+ )
28
+ end
29
+
30
+ it 'allows header attributes to be overwritten' do
31
+ stream_writer.write_header(version: '2.0')
32
+ stream_writer.close
33
+
34
+ expect(stream.string).to eq(
35
+ utf8("<?xml version=\"2.0\" encoding=\"utf-8\"?>\n")
36
+ )
37
+ end
38
+
39
+ it 'raises an error if tags have already been written' do
40
+ expect do
41
+ stream_writer.open_tag('foo') do |foo|
42
+ foo.write_header
43
+ end
44
+ end.to raise_error(XmlWriteStream::InvalidHeaderPositionError)
45
+ end
46
+ end
47
+
48
+ describe '#open_tag' do
49
+ it 'writes an opening tag' do
50
+ stream_writer.open_tag('maytag')
51
+ expect(stream.string).to eq(
52
+ utf8("<maytag>\n</maytag>\n")
53
+ )
54
+ end
55
+
56
+ it 'yields the writer and allows nesting' do
57
+ stream_writer.open_tag('maytag') do |maytag|
58
+ expect(maytag).to be_a(XmlWriteStream::YieldingWriter)
59
+ maytag.open_tag('machine')
60
+ end
61
+
62
+ expect(stream.string).to eq(
63
+ utf8("<maytag>\n <machine>\n </machine>\n</maytag>\n")
64
+ )
65
+ end
66
+
67
+ it 'writes an opening tag with attributes' do
68
+ stream_writer.open_tag('maytag', { type: 'washing_machine' })
69
+ expect(stream.string).to eq(
70
+ utf8("<maytag type=\"washing_machine\">\n</maytag>\n")
71
+ )
72
+ end
73
+
74
+ it 'raises an error if one of the attribute keys is invalid' do
75
+ expect do
76
+ stream_writer.open_tag('maytag', '0foo' => '')
77
+ end.to raise_error(XmlWriteStream::InvalidAttributeKeyError)
78
+ end
79
+
80
+ it 'raises an error if the tag name is invalid' do
81
+ expect do
82
+ stream_writer.open_tag('9foo') {}
83
+ end.to raise_error(XmlWriteStream::InvalidTagNameError)
84
+ end
85
+
86
+ it 'allows digits and colons in the tag name' do
87
+ stream_writer.open_tag('foo9') do |foo|
88
+ foo.open_tag('bar:baz')
89
+ end
90
+
91
+ expect(stream.string).to eq(
92
+ utf8("<foo9>\n <bar:baz>\n </bar:baz>\n</foo9>\n")
93
+ )
94
+ end
95
+
96
+ it 'raises an error if the stream is already closed' do
97
+ stream_writer.close
98
+
99
+ expect do
100
+ stream_writer.open_tag('foo')
101
+ end.to raise_error(XmlWriteStream::EndOfStreamError)
102
+ end
103
+ end
104
+
105
+ describe '#write_text' do
106
+ it 'writes escaped text by default' do
107
+ stream_writer.open_tag('places') do |places|
108
+ places.write_text("Alaska & Hawai'i")
109
+ end
110
+
111
+ expect(stream.string).to eq(
112
+ utf8("<places>\n Alaska &amp; Hawai&apos;i\n</places>\n")
113
+ )
114
+ end
115
+
116
+ it 'writes raw text if asked not to escape' do
117
+ stream_writer.open_tag('places') do |places|
118
+ places.write_text("Alaska & Hawai'i", escape: false)
119
+ end
120
+
121
+ expect(stream.string).to eq(
122
+ utf8("<places>\n Alaska & Hawai'i\n</places>\n")
123
+ )
124
+ end
125
+
126
+ it 'raises an error if no tag has been written yet' do
127
+ expect do
128
+ stream_writer.write_text('foo')
129
+ end.to raise_error(XmlWriteStream::NoTopLevelTagError)
130
+ end
131
+
132
+ it 'raises an error if the stream is already closed' do
133
+ stream_writer.close
134
+
135
+ expect do
136
+ stream_writer.write_text('foo')
137
+ end.to raise_error(XmlWriteStream::EndOfStreamError)
138
+ end
139
+ end
140
+
141
+ describe '#close' do
142
+ it 'closes the stream' do
143
+ stream_writer.close
144
+ expect(stream).to be_closed
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,18 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
+ require 'xml-write-stream/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "xml-write-stream"
6
+ s.version = ::XmlWriteStream::VERSION
7
+ s.authors = ["Cameron Dutro"]
8
+ s.email = ["camertron@gmail.com"]
9
+ s.homepage = "http://github.com/camertron"
10
+
11
+ s.description = s.summary = "An easy, streaming way to generate XML."
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+ s.has_rdoc = true
15
+
16
+ s.require_path = 'lib'
17
+ s.files = Dir["{lib,spec}/**/*", "Gemfile", "History.txt", "README.md", "Rakefile", "xml-write-stream.gemspec"]
18
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xml-write-stream
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: An easy, streaming way to generate XML.
14
+ email:
15
+ - camertron@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/xml-write-stream.rb
21
+ - lib/xml-write-stream/base.rb
22
+ - lib/xml-write-stream/stateful_writer.rb
23
+ - lib/xml-write-stream/version.rb
24
+ - lib/xml-write-stream/yielding_writer.rb
25
+ - spec/spec_helper.rb
26
+ - spec/stateful_spec.rb
27
+ - spec/xml-write-stream_spec.rb
28
+ - spec/yielding_spec.rb
29
+ - Gemfile
30
+ - History.txt
31
+ - README.md
32
+ - Rakefile
33
+ - xml-write-stream.gemspec
34
+ homepage: http://github.com/camertron
35
+ licenses: []
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.1.9
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: An easy, streaming way to generate XML.
57
+ test_files: []