yaml-write-stream 1.0.0
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 +115 -0
- data/Rakefile +18 -0
- data/lib/yaml-write-stream/stateful.rb +183 -0
- data/lib/yaml-write-stream/version.rb +5 -0
- data/lib/yaml-write-stream/yielding.rb +81 -0
- data/lib/yaml-write-stream.rb +54 -0
- data/spec/shared_examples.rb +30 -0
- data/spec/spec_helper.rb +173 -0
- data/spec/stateful_spec.rb +142 -0
- data/spec/yaml-write-stream_spec.rb +146 -0
- data/spec/yielding_spec.rb +32 -0
- data/yaml-write-stream.gemspec +18 -0
- metadata +57 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3b944beb4d212490ae2f8900f090869bbbf1783
|
4
|
+
data.tar.gz: 3d69474c75ca853562dd964956b778220335fbf4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f95dcf646a2930f5459fd021eb0751bca932315269977934ef5e444cc59396091a6d68fa57c527fe86e921dad687661ef2690b99514f691c071e678cdae44c6e
|
7
|
+
data.tar.gz: 075277ab39f967c205400d8a12ca808e81a67df3b375bdd5d63ce45cdc8762e1f8ddb0fa52df57290aa823b4bab35ad809c5869892738a6ad39a17561e882bc2
|
data/Gemfile
ADDED
data/History.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
yaml-write-stream
|
2
|
+
=================
|
3
|
+
|
4
|
+
[](http://travis-ci.org/camertron/yaml-write-stream)
|
5
|
+
|
6
|
+
An easy, streaming way to generate YAML.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
`gem install yaml-write-stream`
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'yaml-write-stream'
|
16
|
+
```
|
17
|
+
|
18
|
+
### Examples for the Impatient
|
19
|
+
|
20
|
+
There are two types of YAML write stream: one that uses blocks and `yield` to delimit arrays (sequences) and objects (maps), 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
|
+
YamlWriteStream.from_stream(stream) do |writer|
|
27
|
+
writer.write_map do |map_writer|
|
28
|
+
map_writer.write_key_value('foo', 'bar')
|
29
|
+
map_writer.write_sequence('baz') do |seq_writer|
|
30
|
+
seq_writer.write_element('goo')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
Stateful:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
stream = StringIO.new
|
40
|
+
writer = YamlWriteStream.from_stream(stream)
|
41
|
+
writer.write_map
|
42
|
+
writer.write_key_value('foo', 'bar')
|
43
|
+
writer.write_sequence('baz')
|
44
|
+
writer.write_element('goo')
|
45
|
+
writer.close # automatically adds closing punctuation for all nested types
|
46
|
+
```
|
47
|
+
|
48
|
+
Output:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
stream.string # => foo: bar\nbaz:\n- goo\n
|
52
|
+
```
|
53
|
+
|
54
|
+
### Yielding Writers
|
55
|
+
|
56
|
+
As far as yielding writers go, the example above contains everything you need. The stream will be automatically closed when the outermost block terminates.
|
57
|
+
|
58
|
+
### Stateful Writers
|
59
|
+
|
60
|
+
Stateful writers have a number of additional methods:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
stream = StringIO.new
|
64
|
+
writer = YamlWriteStream.from_stream(stream)
|
65
|
+
writer.write_map
|
66
|
+
|
67
|
+
writer.in_map? # => true, currently writing a map
|
68
|
+
writer.in_sequence? # => false, not currently writing a sequence
|
69
|
+
writer.eos? # => false, the stream is open and the outermost map hasn't been closed yet
|
70
|
+
|
71
|
+
writer.close_map # explicitly close the current map
|
72
|
+
writer.eos? # => true, the outermost map has been closed
|
73
|
+
|
74
|
+
writer.write_sequence # => raises YamlWriteStream::EndOfStreamError
|
75
|
+
writer.close_sequence # => raises YamlWriteStream::NotInArrayError
|
76
|
+
|
77
|
+
writer.closed? # => false, the stream is still open
|
78
|
+
writer.close # close the stream
|
79
|
+
writer.closed? # => true, the stream has been closed
|
80
|
+
```
|
81
|
+
|
82
|
+
### Writing to a File
|
83
|
+
|
84
|
+
YamlWriteStream also supports streaming to a file via the `open` method:
|
85
|
+
|
86
|
+
Yielding:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
YamlWriteStream.open('path/to/file.yml') do |writer|
|
90
|
+
writer.write_map do |map_writer|
|
91
|
+
...
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
Stateful:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
writer = YamlWriteStream.open('path/to/file.yml')
|
100
|
+
writer.write_map
|
101
|
+
...
|
102
|
+
writer.close
|
103
|
+
```
|
104
|
+
|
105
|
+
## Requirements
|
106
|
+
|
107
|
+
Only Ruby 1.9 or greater is supported (requires the Psych emitter).
|
108
|
+
|
109
|
+
## Running Tests
|
110
|
+
|
111
|
+
`bundle exec rake` should do the trick. Alternatively you can run `bundle exec rspec`, which does the same thing.
|
112
|
+
|
113
|
+
## Authors
|
114
|
+
|
115
|
+
* 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/yaml-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,183 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class YamlWriteStream
|
4
|
+
class NotInMapError < StandardError; end
|
5
|
+
class NotInSequenceError < StandardError; end
|
6
|
+
class EndOfStreamError < StandardError; end
|
7
|
+
|
8
|
+
class StatefulWriter
|
9
|
+
attr_reader :emitter, :stream, :stack, :closed, :first
|
10
|
+
alias :closed? :closed
|
11
|
+
|
12
|
+
def initialize(emitter, stream)
|
13
|
+
@emitter = emitter
|
14
|
+
@stream = stream
|
15
|
+
@stack = []
|
16
|
+
@closed = false
|
17
|
+
after_initialize
|
18
|
+
@first = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def after_initialize
|
22
|
+
@first = true
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
# psych gets confused if you open a file and don't at least
|
27
|
+
# pretend to write something
|
28
|
+
write_scalar('') if first
|
29
|
+
|
30
|
+
until stack.empty?
|
31
|
+
if in_map?
|
32
|
+
close_map
|
33
|
+
else
|
34
|
+
close_sequence
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
emitter.end_document(true)
|
39
|
+
emitter.end_stream
|
40
|
+
stream.close
|
41
|
+
@closed = true
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_map(*args)
|
46
|
+
check_eos
|
47
|
+
@first = false
|
48
|
+
current.write_map(*args) if current
|
49
|
+
stack.push(StatefulMappingWriter.new(emitter, stream))
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_sequence(*args)
|
53
|
+
check_eos
|
54
|
+
@first = false
|
55
|
+
current.write_sequence(*args) if current
|
56
|
+
stack.push(StatefulSequenceWriter.new(emitter, stream))
|
57
|
+
end
|
58
|
+
|
59
|
+
def write_key_value(*args)
|
60
|
+
check_eos
|
61
|
+
@first = false
|
62
|
+
current.write_key_value(*args)
|
63
|
+
end
|
64
|
+
|
65
|
+
def write_element(*args)
|
66
|
+
check_eos
|
67
|
+
@first = false
|
68
|
+
current.write_element(*args)
|
69
|
+
end
|
70
|
+
|
71
|
+
def eos?
|
72
|
+
closed? || (!first && stack.size == 0)
|
73
|
+
end
|
74
|
+
|
75
|
+
def in_map?
|
76
|
+
current ? current.is_map? : false
|
77
|
+
end
|
78
|
+
|
79
|
+
def in_sequence?
|
80
|
+
current ? current.is_sequence? : false
|
81
|
+
end
|
82
|
+
|
83
|
+
def close_map
|
84
|
+
if in_map?
|
85
|
+
stack.pop.close
|
86
|
+
else
|
87
|
+
raise NotInMapError, 'not currently writing a map.'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def close_sequence
|
92
|
+
if in_sequence?
|
93
|
+
stack.pop.close
|
94
|
+
else
|
95
|
+
raise NotInSequenceError, 'not currently writing an sequence.'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def check_eos
|
102
|
+
if eos?
|
103
|
+
raise EndOfStreamError, 'end of stream.'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def current
|
108
|
+
stack.last
|
109
|
+
end
|
110
|
+
|
111
|
+
def write_scalar(value)
|
112
|
+
# value, anchor, tag, plain, quoted, style
|
113
|
+
emitter.scalar(
|
114
|
+
value, nil, nil, true, false, Psych::Nodes::Scalar::ANY
|
115
|
+
)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class StatefulMappingWriter < StatefulWriter
|
120
|
+
def after_initialize
|
121
|
+
# anchor, tag, implicit, style
|
122
|
+
emitter.start_mapping(
|
123
|
+
nil, nil, true, Psych::Nodes::Sequence::BLOCK
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def write_map(key)
|
128
|
+
write_scalar(key)
|
129
|
+
end
|
130
|
+
|
131
|
+
def write_sequence(key)
|
132
|
+
write_scalar(key)
|
133
|
+
end
|
134
|
+
|
135
|
+
def write_key_value(key, value)
|
136
|
+
write_scalar(key)
|
137
|
+
write_scalar(value)
|
138
|
+
end
|
139
|
+
|
140
|
+
def close
|
141
|
+
emitter.end_mapping
|
142
|
+
end
|
143
|
+
|
144
|
+
def is_map?
|
145
|
+
true
|
146
|
+
end
|
147
|
+
|
148
|
+
def is_sequence?
|
149
|
+
false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class StatefulSequenceWriter < StatefulWriter
|
154
|
+
def after_initialize
|
155
|
+
# anchor, tag, implicit, style
|
156
|
+
emitter.start_sequence(
|
157
|
+
nil, nil, true, Psych::Nodes::Sequence::BLOCK
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
def write_element(element)
|
162
|
+
write_scalar(element)
|
163
|
+
end
|
164
|
+
|
165
|
+
def write_map
|
166
|
+
end
|
167
|
+
|
168
|
+
def write_sequence
|
169
|
+
end
|
170
|
+
|
171
|
+
def close
|
172
|
+
emitter.end_sequence
|
173
|
+
end
|
174
|
+
|
175
|
+
def is_map?
|
176
|
+
false
|
177
|
+
end
|
178
|
+
|
179
|
+
def is_sequence?
|
180
|
+
true
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class YamlWriteStream
|
4
|
+
class YieldingWriter
|
5
|
+
attr_reader :emitter, :stream, :first
|
6
|
+
|
7
|
+
def initialize(emitter, stream)
|
8
|
+
@emitter = emitter
|
9
|
+
@stream = stream
|
10
|
+
@first = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def close
|
14
|
+
# psych gets confused if you open a file and don't at least
|
15
|
+
# pretend to write something
|
16
|
+
write_scalar('') if first
|
17
|
+
emitter.end_document(true)
|
18
|
+
emitter.end_stream
|
19
|
+
stream.close
|
20
|
+
end
|
21
|
+
|
22
|
+
def write_sequence
|
23
|
+
@first = false
|
24
|
+
|
25
|
+
# anchor, tag, implicit, style
|
26
|
+
emitter.start_sequence(
|
27
|
+
nil, nil, true, Psych::Nodes::Sequence::ANY
|
28
|
+
)
|
29
|
+
|
30
|
+
yield YieldingSequenceWriter.new(emitter, stream)
|
31
|
+
emitter.end_sequence
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_map
|
35
|
+
@first = false
|
36
|
+
|
37
|
+
# anchor, tag, implicit, style
|
38
|
+
emitter.start_mapping(
|
39
|
+
nil, nil, true, Psych::Nodes::Sequence::ANY
|
40
|
+
)
|
41
|
+
|
42
|
+
yield YieldingMappingWriter.new(emitter, stream)
|
43
|
+
emitter.end_mapping
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def write_scalar(value)
|
49
|
+
@first = false
|
50
|
+
|
51
|
+
# value, anchor, tag, plain, quoted, style
|
52
|
+
emitter.scalar(
|
53
|
+
value, nil, nil, true, false, Psych::Nodes::Scalar::ANY
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class YieldingMappingWriter < YieldingWriter
|
59
|
+
def write_map(key)
|
60
|
+
write_scalar(key)
|
61
|
+
super()
|
62
|
+
end
|
63
|
+
|
64
|
+
def write_sequence(key)
|
65
|
+
write_scalar(key)
|
66
|
+
super()
|
67
|
+
end
|
68
|
+
|
69
|
+
def write_key_value(key, value)
|
70
|
+
@first = false
|
71
|
+
write_scalar(key)
|
72
|
+
write_scalar(value)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class YieldingSequenceWriter < YieldingWriter
|
77
|
+
def write_element(element)
|
78
|
+
write_scalar(element)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'psych'
|
4
|
+
require 'yaml-write-stream/yielding'
|
5
|
+
require 'yaml-write-stream/stateful'
|
6
|
+
|
7
|
+
class YamlWriteStream
|
8
|
+
class << self
|
9
|
+
def open(path, encoding = Psych::Parser::UTF8, &block)
|
10
|
+
handle = ::File.open(path, 'w')
|
11
|
+
from_stream(handle, encoding, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_stream(stream, encoding = Psych::Parser::UTF8)
|
15
|
+
emitter = Psych::Emitter.new(stream)
|
16
|
+
emitter.start_stream(convert_encoding(encoding))
|
17
|
+
|
18
|
+
# version, tag_directives, implicit
|
19
|
+
emitter.start_document([], [], true)
|
20
|
+
|
21
|
+
if block_given?
|
22
|
+
yield writer = YieldingWriter.new(emitter, stream)
|
23
|
+
writer.close
|
24
|
+
nil
|
25
|
+
else
|
26
|
+
StatefulWriter.new(emitter, stream)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def convert_encoding(encoding)
|
33
|
+
case encoding
|
34
|
+
when Encoding
|
35
|
+
case encoding
|
36
|
+
when Encoding::UTF_8
|
37
|
+
Psych::Parser::UTF8
|
38
|
+
when Encoding::UTF_16BE
|
39
|
+
Psych::Parser::UTF16BE
|
40
|
+
when Encoding::UTF_16LE
|
41
|
+
Psych::Parser::UTF16LE
|
42
|
+
else
|
43
|
+
raise ArgumentError, "'#{encoding}' encoding is not supported by Psych."
|
44
|
+
end
|
45
|
+
when Fixnum
|
46
|
+
encoding
|
47
|
+
when String
|
48
|
+
convert_encoding(Encoding.find(encoding))
|
49
|
+
else
|
50
|
+
raise ArgumentError, "encoding of type #{encoding.class} is not supported, please provide an Encoding or a Fixnum."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
shared_examples 'a yaml stream' do
|
4
|
+
it 'handles a simple array' do
|
5
|
+
check_roundtrip(['abc'])
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'handles a simple object' do
|
9
|
+
check_roundtrip({ 'foo' => 'bar' })
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'handles one level of array nesting' do
|
13
|
+
check_roundtrip([['def'],'abc'])
|
14
|
+
check_roundtrip(['abc',['def']])
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'handles one level of object nesting' do
|
18
|
+
check_roundtrip({ 'foo' => { 'bar' => 'baz' } })
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'handles one level of mixed nesting' do
|
22
|
+
check_roundtrip({ 'foo' => ['bar', 'baz'] })
|
23
|
+
check_roundtrip([{ 'foo' => 'bar' }])
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'handles multiple levels of mixed nesting' do
|
27
|
+
check_roundtrip({'foo' => ['bar', { 'baz' => 'moo', 'gaz' => ['doo'] }, 'kal'], 'jim' => ['jill', ['john']] })
|
28
|
+
check_roundtrip(['foo', { 'bar' => 'baz', 'moo' => ['gaz', ['jim', ['jill']], 'jam'] }])
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
require 'yaml-write-stream'
|
5
|
+
require 'shared_examples'
|
6
|
+
require 'pry-nav'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.mock_with :rr
|
10
|
+
end
|
11
|
+
|
12
|
+
class RoundtripChecker
|
13
|
+
class << self
|
14
|
+
include RSpec::Matchers
|
15
|
+
|
16
|
+
def check_roundtrip(obj)
|
17
|
+
stream = StringIO.new
|
18
|
+
writer = create_writer(stream)
|
19
|
+
serialize(obj, writer)
|
20
|
+
writer.close
|
21
|
+
new_obj = Psych.load(stream.string)
|
22
|
+
compare(obj, new_obj)
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def create_emitter(stream)
|
28
|
+
Psych::Emitter.new(stream).tap do |emitter|
|
29
|
+
emitter.start_stream(Psych::Parser::UTF8)
|
30
|
+
emitter.start_document([], [], true)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def compare(old_obj, new_obj)
|
37
|
+
expect(old_obj.class).to equal(new_obj.class)
|
38
|
+
|
39
|
+
case old_obj
|
40
|
+
when Hash
|
41
|
+
expect(old_obj.keys).to eq(new_obj.keys)
|
42
|
+
|
43
|
+
old_obj.each_pair do |key, old_val|
|
44
|
+
compare(old_val, new_obj[key])
|
45
|
+
end
|
46
|
+
when Array
|
47
|
+
old_obj.each_with_index do |old_element, idx|
|
48
|
+
compare(old_element, new_obj[idx])
|
49
|
+
end
|
50
|
+
else
|
51
|
+
expect(old_obj).to eq(new_obj)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class YieldingRoundtripChecker < RoundtripChecker
|
58
|
+
class << self
|
59
|
+
def create_writer(stream)
|
60
|
+
YamlWriteStream::YieldingWriter.new(
|
61
|
+
create_emitter(stream), stream
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def serialize(obj, writer)
|
68
|
+
case obj
|
69
|
+
when Hash
|
70
|
+
writer.write_map do |map_writer|
|
71
|
+
serialize_map(obj, map_writer)
|
72
|
+
end
|
73
|
+
when Array
|
74
|
+
writer.write_sequence do |sequence_writer|
|
75
|
+
serialize_sequence(obj, sequence_writer)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def serialize_map(obj, writer)
|
81
|
+
obj.each_pair do |key, val|
|
82
|
+
case val
|
83
|
+
when Hash
|
84
|
+
writer.write_map(key) do |map_writer|
|
85
|
+
serialize_map(val, map_writer)
|
86
|
+
end
|
87
|
+
when Array
|
88
|
+
writer.write_sequence(key) do |sequence_writer|
|
89
|
+
serialize_sequence(val, sequence_writer)
|
90
|
+
end
|
91
|
+
else
|
92
|
+
writer.write_key_value(key, val)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def serialize_sequence(obj, writer)
|
98
|
+
obj.each do |element|
|
99
|
+
case element
|
100
|
+
when Hash
|
101
|
+
writer.write_map do |map_writer|
|
102
|
+
serialize_map(element, map_writer)
|
103
|
+
end
|
104
|
+
when Array
|
105
|
+
writer.write_sequence do |sequence_writer|
|
106
|
+
serialize_sequence(element, sequence_writer)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
writer.write_element(element)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class StatefulRoundtripChecker < RoundtripChecker
|
117
|
+
class << self
|
118
|
+
def create_writer(stream)
|
119
|
+
YamlWriteStream::StatefulWriter.new(
|
120
|
+
create_emitter(stream), stream
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
def serialize(obj, writer)
|
127
|
+
case obj
|
128
|
+
when Hash
|
129
|
+
writer.write_map
|
130
|
+
serialize_map(obj, writer)
|
131
|
+
writer.close_map
|
132
|
+
when Array
|
133
|
+
writer.write_sequence
|
134
|
+
serialize_sequence(obj, writer)
|
135
|
+
writer.close_sequence
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def serialize_map(obj, writer)
|
140
|
+
obj.each_pair do |key, val|
|
141
|
+
case val
|
142
|
+
when Hash
|
143
|
+
writer.write_map(key)
|
144
|
+
serialize_map(val, writer)
|
145
|
+
writer.close_map
|
146
|
+
when Array
|
147
|
+
writer.write_sequence(key)
|
148
|
+
serialize_sequence(val, writer)
|
149
|
+
writer.close_sequence
|
150
|
+
else
|
151
|
+
writer.write_key_value(key, val)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def serialize_sequence(obj, writer)
|
157
|
+
obj.each do |element|
|
158
|
+
case element
|
159
|
+
when Hash
|
160
|
+
writer.write_map
|
161
|
+
serialize_map(element, writer)
|
162
|
+
writer.close_map
|
163
|
+
when Array
|
164
|
+
writer.write_sequence
|
165
|
+
serialize_sequence(element, writer)
|
166
|
+
writer.close_sequence
|
167
|
+
else
|
168
|
+
writer.write_element(element)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe YamlWriteStream::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
|
+
StatefulRoundtripChecker.create_writer(stream)
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_roundtrip(obj)
|
17
|
+
StatefulRoundtripChecker.check_roundtrip(obj)
|
18
|
+
end
|
19
|
+
|
20
|
+
def utf8(str)
|
21
|
+
str.encode(Encoding::UTF_8)
|
22
|
+
end
|
23
|
+
|
24
|
+
it_behaves_like 'a yaml stream'
|
25
|
+
|
26
|
+
describe '#close' do
|
27
|
+
it 'unwinds the stack, adds appropriate closing punctuation for each unclosed item, and closes the stream' do
|
28
|
+
stream_writer.write_sequence
|
29
|
+
stream_writer.write_element('abc')
|
30
|
+
stream_writer.write_map
|
31
|
+
stream_writer.write_key_value('def', 'ghi')
|
32
|
+
stream_writer.close
|
33
|
+
|
34
|
+
expect(stream.string).to eq(utf8("- abc\n- def: ghi\n"))
|
35
|
+
expect(stream_writer).to be_closed
|
36
|
+
expect(stream).to be_closed
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#closed?' do
|
41
|
+
it 'returns false if the stream is still open' do
|
42
|
+
expect(stream_writer).to_not be_closed
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns true if the stream is closed' do
|
46
|
+
stream_writer.close
|
47
|
+
expect(stream_writer).to be_closed
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#in_map?' do
|
52
|
+
it 'returns true if the writer is currently writing a map' do
|
53
|
+
stream_writer.write_map
|
54
|
+
expect(stream_writer).to be_in_map
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'returns false if the writer is not currently writing a map' do
|
58
|
+
expect(stream_writer).to_not be_in_map
|
59
|
+
stream_writer.write_sequence
|
60
|
+
expect(stream_writer).to_not be_in_map
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#in_sequence?' do
|
65
|
+
it 'returns true if the writer is currently writing a sequence' do
|
66
|
+
stream_writer.write_sequence
|
67
|
+
expect(stream_writer).to be_in_sequence
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns false if the writer is not currently writing a sequence' do
|
71
|
+
expect(stream_writer).to_not be_in_sequence
|
72
|
+
stream_writer.write_map
|
73
|
+
expect(stream_writer).to_not be_in_sequence
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#eos?' do
|
78
|
+
it 'returns false if nothing has been written yet' do
|
79
|
+
expect(stream_writer).to_not be_eos
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns false if the writer is in the middle of writing' do
|
83
|
+
stream_writer.write_map
|
84
|
+
expect(stream_writer).to_not be_eos
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns true if the writer has finished it's top-level" do
|
88
|
+
stream_writer.write_map
|
89
|
+
stream_writer.close_map
|
90
|
+
expect(stream_writer).to be_eos
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'returns true if the writer is closed' do
|
94
|
+
stream_writer.close
|
95
|
+
expect(stream_writer).to be_eos
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#close_map' do
|
100
|
+
it 'raises an error if a map is not currently being written' do
|
101
|
+
stream_writer.write_sequence
|
102
|
+
expect(lambda { stream_writer.close_map }).to raise_error(YamlWriteStream::NotInMapError)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#close_sequence' do
|
107
|
+
it 'raises an error if a sequence is not currently being written' do
|
108
|
+
stream_writer.write_map
|
109
|
+
expect(lambda { stream_writer.close_sequence }).to raise_error(YamlWriteStream::NotInSequenceError)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'with a closed stream writer' do
|
114
|
+
before(:each) do
|
115
|
+
stream_writer.close
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#write_map' do
|
119
|
+
it 'raises an error if eos' do
|
120
|
+
expect(lambda { stream_writer.write_map }).to raise_error(YamlWriteStream::EndOfStreamError)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#write_sequence' do
|
125
|
+
it 'raises an error if eos' do
|
126
|
+
expect(lambda { stream_writer.write_map }).to raise_error(YamlWriteStream::EndOfStreamError)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#write_key_value' do
|
131
|
+
it 'raises an error if eos' do
|
132
|
+
expect(lambda { stream_writer.write_key_value('abc', 'def') }).to raise_error(YamlWriteStream::EndOfStreamError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '#write_element' do
|
137
|
+
it 'raises an error if eos' do
|
138
|
+
expect(lambda { stream_writer.write_element('foo') }).to raise_error(YamlWriteStream::EndOfStreamError)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
describe YamlWriteStream do
|
7
|
+
let(:yielding_writer) { YamlWriteStream::YieldingWriter }
|
8
|
+
let(:stateful_writer) { YamlWriteStream::StatefulWriter }
|
9
|
+
let(:stream_writer) { YamlWriteStream }
|
10
|
+
let(:tempfile) { Tempfile.new('temp') }
|
11
|
+
let(:stream) { StringIO.new }
|
12
|
+
|
13
|
+
def encodings_match?(outputs)
|
14
|
+
flattened = flatten_encodings(outputs)
|
15
|
+
flattened.uniq.size == flattened.size
|
16
|
+
end
|
17
|
+
|
18
|
+
def flatten_encodings(outputs)
|
19
|
+
outputs.map do |encoding, value|
|
20
|
+
bom = case encoding
|
21
|
+
when Encoding::UTF_16LE, Encoding::UTF_16BE
|
22
|
+
[239, 187, 191]
|
23
|
+
else
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
|
27
|
+
bom + value
|
28
|
+
.force_encoding(encoding)
|
29
|
+
.encode(Encoding::UTF_8)
|
30
|
+
.bytes
|
31
|
+
.to_a
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#from_stream' do
|
36
|
+
it 'yields a yielding stream if given a block' do
|
37
|
+
stream_writer.from_stream(stream) do |writer|
|
38
|
+
expect(writer).to be_a(yielding_writer)
|
39
|
+
expect(writer.stream).to equal(stream)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'returns a stateful writer if not given a block' do
|
44
|
+
writer = stream_writer.from_stream(stream)
|
45
|
+
expect(writer).to be_a(stateful_writer)
|
46
|
+
expect(writer.stream).to equal(stream)
|
47
|
+
end
|
48
|
+
|
49
|
+
[Encoding::UTF_8, Encoding::UTF_16LE, Encoding::UTF_16BE].each do |encoding|
|
50
|
+
it "supports specifying a #{encoding.name} encoding" do
|
51
|
+
stream_writer.from_stream(stream, encoding) do |writer|
|
52
|
+
writer.write_map do |map_writer|
|
53
|
+
map_writer.write_key_value('foo', 'bar')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
expect(
|
58
|
+
encodings_match?({
|
59
|
+
encoding => stream.string,
|
60
|
+
Encoding::UTF_8 => "foo: bar\n"
|
61
|
+
})
|
62
|
+
).to eq(true)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "doesn't support other encodings" do
|
67
|
+
expect(
|
68
|
+
lambda do
|
69
|
+
stream_writer.from_stream(stream, Encoding::US_ASCII)
|
70
|
+
end
|
71
|
+
).to raise_error(ArgumentError)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'interprets string encoding names' do
|
75
|
+
stream_writer.from_stream(stream, 'UTF-16BE') do |writer|
|
76
|
+
writer.write_map do |map_writer|
|
77
|
+
map_writer.write_key_value('foo', 'bar')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
expect(
|
82
|
+
encodings_match?({
|
83
|
+
Encoding::UTF_16BE => stream.string,
|
84
|
+
Encoding::UTF_8 => "foo: bar\n"
|
85
|
+
})
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'interprets Psych integer encodings' do
|
90
|
+
stream_writer.from_stream(stream, Psych::Parser::UTF16BE) do |writer|
|
91
|
+
writer.write_map do |map_writer|
|
92
|
+
map_writer.write_key_value('foo', 'bar')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
expect(
|
97
|
+
encodings_match?({
|
98
|
+
Encoding::UTF_16BE => stream.string,
|
99
|
+
Encoding::UTF_8 => "foo: bar\n"
|
100
|
+
})
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'raises an error if an unrecognized type of object is given as encoding' do
|
105
|
+
expect(
|
106
|
+
lambda do
|
107
|
+
stream_writer.from_stream(stream, Object.new)
|
108
|
+
end
|
109
|
+
).to raise_error(ArgumentError)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#open' do
|
114
|
+
it 'opens a file and yields a yielding stream if given a block' do
|
115
|
+
mock.proxy(File).open(tempfile, 'w')
|
116
|
+
stream_writer.open(tempfile) do |writer|
|
117
|
+
expect(writer).to be_a(yielding_writer)
|
118
|
+
expect(writer.stream.path).to eq(tempfile.path)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'opens a file and returns a stateful writer if not given a block' do
|
123
|
+
mock.proxy(File).open(tempfile, 'w')
|
124
|
+
writer = stream_writer.open(tempfile)
|
125
|
+
expect(writer).to be_a(stateful_writer)
|
126
|
+
expect(writer.stream.path).to eq(tempfile.path)
|
127
|
+
end
|
128
|
+
|
129
|
+
[Encoding::UTF_8, Encoding::UTF_16LE, Encoding::UTF_16BE].each do |encoding|
|
130
|
+
it "supports specifying a #{encoding.name} encoding" do
|
131
|
+
stream_writer.open(tempfile, encoding) do |writer|
|
132
|
+
writer.write_map do |map_writer|
|
133
|
+
map_writer.write_key_value('foo', 'bar')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
expect(
|
138
|
+
encodings_match?({
|
139
|
+
encoding => stream.string,
|
140
|
+
Encoding::UTF_8 => "foo: bar\n"
|
141
|
+
})
|
142
|
+
).to eq(true)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe YamlWriteStream::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
|
+
YieldingRoundtripChecker.create_writer(stream)
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_roundtrip(obj)
|
17
|
+
YieldingRoundtripChecker.check_roundtrip(obj)
|
18
|
+
end
|
19
|
+
|
20
|
+
def utf8(str)
|
21
|
+
str.encode(Encoding::UTF_8)
|
22
|
+
end
|
23
|
+
|
24
|
+
it_behaves_like 'a yaml stream'
|
25
|
+
|
26
|
+
describe '#close' do
|
27
|
+
it 'closes the underlying stream' do
|
28
|
+
stream_writer.close
|
29
|
+
expect(stream).to be_closed
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), 'lib')
|
2
|
+
require 'yaml-write-stream/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "yaml-write-stream"
|
6
|
+
s.version = ::YamlWriteStream::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 YAML."
|
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", "yaml-write-stream.gemspec"]
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yaml-write-stream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cameron Dutro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-26 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: An easy, streaming way to generate YAML.
|
14
|
+
email:
|
15
|
+
- camertron@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/yaml-write-stream/stateful.rb
|
21
|
+
- lib/yaml-write-stream/version.rb
|
22
|
+
- lib/yaml-write-stream/yielding.rb
|
23
|
+
- lib/yaml-write-stream.rb
|
24
|
+
- spec/shared_examples.rb
|
25
|
+
- spec/spec_helper.rb
|
26
|
+
- spec/stateful_spec.rb
|
27
|
+
- spec/yaml-write-stream_spec.rb
|
28
|
+
- spec/yielding_spec.rb
|
29
|
+
- Gemfile
|
30
|
+
- History.txt
|
31
|
+
- README.md
|
32
|
+
- Rakefile
|
33
|
+
- yaml-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.0.14
|
54
|
+
signing_key:
|
55
|
+
specification_version: 4
|
56
|
+
summary: An easy, streaming way to generate YAML.
|
57
|
+
test_files: []
|