xml-write-stream 1.0.2

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.
@@ -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: []