xml-write-stream 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
|
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: []
|