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.
- checksums.yaml +7 -0
- data/Gemfile +14 -0
- data/History.txt +3 -0
- data/README.md +110 -0
- data/Rakefile +18 -0
- data/lib/xml-write-stream.rb +40 -0
- data/lib/xml-write-stream/base.rb +112 -0
- data/lib/xml-write-stream/stateful_writer.rb +87 -0
- data/lib/xml-write-stream/version.rb +5 -0
- data/lib/xml-write-stream/yielding_writer.rb +65 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/stateful_spec.rb +192 -0
- data/spec/xml-write-stream_spec.rb +77 -0
- data/spec/yielding_spec.rb +147 -0
- data/xml-write-stream.gemspec +18 -0
- metadata +57 -0
checksums.yaml
ADDED
@@ -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
data/History.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
xml-write-stream
|
2
|
+
=================
|
3
|
+
|
4
|
+
[](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
|
data/Rakefile
ADDED
@@ -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
|
+
'"' => '"',
|
16
|
+
"'" => ''',
|
17
|
+
'<' => '<',
|
18
|
+
'>' => '>',
|
19
|
+
'&' => '&'
|
20
|
+
}
|
21
|
+
|
22
|
+
ATTRIBUTE_ESCAPE_HASH = TEXT_ESCAPE_HASH.merge({
|
23
|
+
"\n" => '
',
|
24
|
+
"\r" => '
',
|
25
|
+
"\t" => '	'
|
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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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 & Hawai'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 & Hawai'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: []
|