Merge pull request #335 from rogpeppe/024-merge-devel
merge devel into v2
diff --git a/.travis.yml b/.travis.yml
index 004172a..9f55693 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,9 @@
- 1.4
- 1.5
- 1.6
+ - 1.7
+ - 1.8
+ - 1.9
- tip
go_import_path: gopkg.in/yaml.v2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..866d74a
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,13 @@
+Copyright 2011-2016 Canonical Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/apic.go b/apic.go
index 95ec014..3e24a0d 100644
--- a/apic.go
+++ b/apic.go
@@ -2,7 +2,6 @@
import (
"io"
- "os"
)
func yaml_insert_token(parser *yaml_parser_t, pos int, token *yaml_token_t) {
@@ -48,9 +47,9 @@
return n, nil
}
-// File read handler.
-func yaml_file_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
- return parser.input_file.Read(buffer)
+// Reader read handler.
+func yaml_reader_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) {
+ return parser.input_reader.Read(buffer)
}
// Set a string input.
@@ -64,12 +63,12 @@
}
// Set a file input.
-func yaml_parser_set_input_file(parser *yaml_parser_t, file *os.File) {
+func yaml_parser_set_input_reader(parser *yaml_parser_t, r io.Reader) {
if parser.read_handler != nil {
panic("must set the input source only once")
}
- parser.read_handler = yaml_file_read_handler
- parser.input_file = file
+ parser.read_handler = yaml_reader_read_handler
+ parser.input_reader = r
}
// Set the source encoding.
@@ -81,14 +80,13 @@
}
// Create a new emitter object.
-func yaml_emitter_initialize(emitter *yaml_emitter_t) bool {
+func yaml_emitter_initialize(emitter *yaml_emitter_t) {
*emitter = yaml_emitter_t{
buffer: make([]byte, output_buffer_size),
raw_buffer: make([]byte, 0, output_raw_buffer_size),
states: make([]yaml_emitter_state_t, 0, initial_stack_size),
events: make([]yaml_event_t, 0, initial_queue_size),
}
- return true
}
// Destroy an emitter object.
@@ -102,9 +100,10 @@
return nil
}
-// File write handler.
-func yaml_file_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
- _, err := emitter.output_file.Write(buffer)
+// yaml_writer_write_handler uses emitter.output_writer to write the
+// emitted text.
+func yaml_writer_write_handler(emitter *yaml_emitter_t, buffer []byte) error {
+ _, err := emitter.output_writer.Write(buffer)
return err
}
@@ -118,12 +117,12 @@
}
// Set a file output.
-func yaml_emitter_set_output_file(emitter *yaml_emitter_t, file io.Writer) {
+func yaml_emitter_set_output_writer(emitter *yaml_emitter_t, w io.Writer) {
if emitter.write_handler != nil {
panic("must set the output target only once")
}
- emitter.write_handler = yaml_file_write_handler
- emitter.output_file = file
+ emitter.write_handler = yaml_writer_write_handler
+ emitter.output_writer = w
}
// Set the output encoding.
@@ -252,41 +251,41 @@
//
// Create STREAM-START.
-func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) bool {
+func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) {
*event = yaml_event_t{
typ: yaml_STREAM_START_EVENT,
encoding: encoding,
}
- return true
}
// Create STREAM-END.
-func yaml_stream_end_event_initialize(event *yaml_event_t) bool {
+func yaml_stream_end_event_initialize(event *yaml_event_t) {
*event = yaml_event_t{
typ: yaml_STREAM_END_EVENT,
}
- return true
}
// Create DOCUMENT-START.
-func yaml_document_start_event_initialize(event *yaml_event_t, version_directive *yaml_version_directive_t,
- tag_directives []yaml_tag_directive_t, implicit bool) bool {
+func yaml_document_start_event_initialize(
+ event *yaml_event_t,
+ version_directive *yaml_version_directive_t,
+ tag_directives []yaml_tag_directive_t,
+ implicit bool,
+) {
*event = yaml_event_t{
typ: yaml_DOCUMENT_START_EVENT,
version_directive: version_directive,
tag_directives: tag_directives,
implicit: implicit,
}
- return true
}
// Create DOCUMENT-END.
-func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) bool {
+func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) {
*event = yaml_event_t{
typ: yaml_DOCUMENT_END_EVENT,
implicit: implicit,
}
- return true
}
///*
@@ -348,7 +347,7 @@
}
// Create MAPPING-START.
-func yaml_mapping_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_mapping_style_t) bool {
+func yaml_mapping_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_mapping_style_t) {
*event = yaml_event_t{
typ: yaml_MAPPING_START_EVENT,
anchor: anchor,
@@ -356,15 +355,13 @@
implicit: implicit,
style: yaml_style_t(style),
}
- return true
}
// Create MAPPING-END.
-func yaml_mapping_end_event_initialize(event *yaml_event_t) bool {
+func yaml_mapping_end_event_initialize(event *yaml_event_t) {
*event = yaml_event_t{
typ: yaml_MAPPING_END_EVENT,
}
- return true
}
// Destroy an event object.
diff --git a/decode.go b/decode.go
index e85eb2e..c8eac16 100644
--- a/decode.go
+++ b/decode.go
@@ -4,6 +4,7 @@
"encoding"
"encoding/base64"
"fmt"
+ "io"
"math"
"reflect"
"strconv"
@@ -22,19 +23,22 @@
kind int
line, column int
tag string
- value string
- implicit bool
- children []*node
- anchors map[string]*node
+ // For an alias node, alias holds the resolved alias.
+ alias *node
+ value string
+ implicit bool
+ children []*node
+ anchors map[string]*node
}
// ----------------------------------------------------------------------------
// Parser, produces a node tree out of a libyaml event stream.
type parser struct {
- parser yaml_parser_t
- event yaml_event_t
- doc *node
+ parser yaml_parser_t
+ event yaml_event_t
+ doc *node
+ doneInit bool
}
func newParser(b []byte) *parser {
@@ -42,21 +46,30 @@
if !yaml_parser_initialize(&p.parser) {
panic("failed to initialize YAML emitter")
}
-
if len(b) == 0 {
b = []byte{'\n'}
}
-
yaml_parser_set_input_string(&p.parser, b)
-
- p.skip()
- if p.event.typ != yaml_STREAM_START_EVENT {
- panic("expected stream start event, got " + strconv.Itoa(int(p.event.typ)))
- }
- p.skip()
return &p
}
+func newParserFromReader(r io.Reader) *parser {
+ p := parser{}
+ if !yaml_parser_initialize(&p.parser) {
+ panic("failed to initialize YAML emitter")
+ }
+ yaml_parser_set_input_reader(&p.parser, r)
+ return &p
+}
+
+func (p *parser) init() {
+ if p.doneInit {
+ return
+ }
+ p.expect(yaml_STREAM_START_EVENT)
+ p.doneInit = true
+}
+
func (p *parser) destroy() {
if p.event.typ != yaml_NO_EVENT {
yaml_event_delete(&p.event)
@@ -64,16 +77,35 @@
yaml_parser_delete(&p.parser)
}
-func (p *parser) skip() {
- if p.event.typ != yaml_NO_EVENT {
- if p.event.typ == yaml_STREAM_END_EVENT {
- failf("attempted to go past the end of stream; corrupted value?")
+// expect consumes an event from the event stream and
+// checks that it's of the expected type.
+func (p *parser) expect(e yaml_event_type_t) {
+ if p.event.typ == yaml_NO_EVENT {
+ if !yaml_parser_parse(&p.parser, &p.event) {
+ p.fail()
}
- yaml_event_delete(&p.event)
+ }
+ if p.event.typ == yaml_STREAM_END_EVENT {
+ failf("attempted to go past the end of stream; corrupted value?")
+ }
+ if p.event.typ != e {
+ p.parser.problem = fmt.Sprintf("expected %s event but got %s", e, p.event.typ)
+ p.fail()
+ }
+ yaml_event_delete(&p.event)
+ p.event.typ = yaml_NO_EVENT
+}
+
+// peek peeks at the next event in the event stream,
+// puts the results into p.event and returns the event type.
+func (p *parser) peek() yaml_event_type_t {
+ if p.event.typ != yaml_NO_EVENT {
+ return p.event.typ
}
if !yaml_parser_parse(&p.parser, &p.event) {
p.fail()
}
+ return p.event.typ
}
func (p *parser) fail() {
@@ -103,7 +135,8 @@
}
func (p *parser) parse() *node {
- switch p.event.typ {
+ p.init()
+ switch p.peek() {
case yaml_SCALAR_EVENT:
return p.scalar()
case yaml_ALIAS_EVENT:
@@ -118,7 +151,7 @@
// Happens when attempting to decode an empty buffer.
return nil
default:
- panic("attempted to parse unknown event: " + strconv.Itoa(int(p.event.typ)))
+ panic("attempted to parse unknown event: " + p.event.typ.String())
}
}
@@ -134,19 +167,20 @@
n := p.node(documentNode)
n.anchors = make(map[string]*node)
p.doc = n
- p.skip()
+ p.expect(yaml_DOCUMENT_START_EVENT)
n.children = append(n.children, p.parse())
- if p.event.typ != yaml_DOCUMENT_END_EVENT {
- panic("expected end of document event but got " + strconv.Itoa(int(p.event.typ)))
- }
- p.skip()
+ p.expect(yaml_DOCUMENT_END_EVENT)
return n
}
func (p *parser) alias() *node {
n := p.node(aliasNode)
n.value = string(p.event.anchor)
- p.skip()
+ n.alias = p.doc.anchors[n.value]
+ if n.alias == nil {
+ failf("unknown anchor '%s' referenced", n.value)
+ }
+ p.expect(yaml_ALIAS_EVENT)
return n
}
@@ -156,29 +190,29 @@
n.tag = string(p.event.tag)
n.implicit = p.event.implicit
p.anchor(n, p.event.anchor)
- p.skip()
+ p.expect(yaml_SCALAR_EVENT)
return n
}
func (p *parser) sequence() *node {
n := p.node(sequenceNode)
p.anchor(n, p.event.anchor)
- p.skip()
- for p.event.typ != yaml_SEQUENCE_END_EVENT {
+ p.expect(yaml_SEQUENCE_START_EVENT)
+ for p.peek() != yaml_SEQUENCE_END_EVENT {
n.children = append(n.children, p.parse())
}
- p.skip()
+ p.expect(yaml_SEQUENCE_END_EVENT)
return n
}
func (p *parser) mapping() *node {
n := p.node(mappingNode)
p.anchor(n, p.event.anchor)
- p.skip()
- for p.event.typ != yaml_MAPPING_END_EVENT {
+ p.expect(yaml_MAPPING_START_EVENT)
+ for p.peek() != yaml_MAPPING_END_EVENT {
n.children = append(n.children, p.parse(), p.parse())
}
- p.skip()
+ p.expect(yaml_MAPPING_END_EVENT)
return n
}
@@ -187,7 +221,7 @@
type decoder struct {
doc *node
- aliases map[string]bool
+ aliases map[*node]bool
mapType reflect.Type
terrors []string
strict bool
@@ -198,11 +232,13 @@
durationType = reflect.TypeOf(time.Duration(0))
defaultMapType = reflect.TypeOf(map[interface{}]interface{}{})
ifaceType = defaultMapType.Elem()
+ timeType = reflect.TypeOf(time.Time{})
+ ptrTimeType = reflect.TypeOf(&time.Time{})
)
func newDecoder(strict bool) *decoder {
d := &decoder{mapType: defaultMapType, strict: strict}
- d.aliases = make(map[string]bool)
+ d.aliases = make(map[*node]bool)
return d
}
@@ -308,16 +344,13 @@
}
func (d *decoder) alias(n *node, out reflect.Value) (good bool) {
- an, ok := d.doc.anchors[n.value]
- if !ok {
- failf("unknown anchor '%s' referenced", n.value)
- }
- if d.aliases[n.value] {
+ if d.aliases[n] {
+ // TODO this could actually be allowed in some circumstances.
failf("anchor '%s' value contains itself", n.value)
}
- d.aliases[n.value] = true
- good = d.unmarshal(an, out)
- delete(d.aliases, n.value)
+ d.aliases[n] = true
+ good = d.unmarshal(n.alias, out)
+ delete(d.aliases, n)
return good
}
@@ -329,7 +362,7 @@
}
}
-func (d *decoder) scalar(n *node, out reflect.Value) (good bool) {
+func (d *decoder) scalar(n *node, out reflect.Value) bool {
var tag string
var resolved interface{}
if n.tag == "" && !n.implicit {
@@ -353,9 +386,26 @@
}
return true
}
- if s, ok := resolved.(string); ok && out.CanAddr() {
- if u, ok := out.Addr().Interface().(encoding.TextUnmarshaler); ok {
- err := u.UnmarshalText([]byte(s))
+ if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() {
+ // We've resolved to exactly the type we want, so use that.
+ out.Set(resolvedv)
+ return true
+ }
+ // Perhaps we can use the value as a TextUnmarshaler to
+ // set its value.
+ if out.CanAddr() {
+ u, ok := out.Addr().Interface().(encoding.TextUnmarshaler)
+ if ok {
+ var text []byte
+ if tag == yaml_BINARY_TAG {
+ text = []byte(resolved.(string))
+ } else {
+ // We let any value be unmarshaled into TextUnmarshaler.
+ // That might be more lax than we'd like, but the
+ // TextUnmarshaler itself should bowl out any dubious values.
+ text = []byte(n.value)
+ }
+ err := u.UnmarshalText(text)
if err != nil {
fail(err)
}
@@ -366,46 +416,53 @@
case reflect.String:
if tag == yaml_BINARY_TAG {
out.SetString(resolved.(string))
- good = true
- } else if resolved != nil {
+ return true
+ }
+ if resolved != nil {
out.SetString(n.value)
- good = true
+ return true
}
case reflect.Interface:
if resolved == nil {
out.Set(reflect.Zero(out.Type()))
+ } else if tag == yaml_TIMESTAMP_TAG {
+ // It looks like a timestamp but for backward compatibility
+ // reasons we set it as a string, so that code that unmarshals
+ // timestamp-like values into interface{} will continue to
+ // see a string and not a time.Time.
+ out.Set(reflect.ValueOf(n.value))
} else {
out.Set(reflect.ValueOf(resolved))
}
- good = true
+ return true
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
switch resolved := resolved.(type) {
case int:
if !out.OverflowInt(int64(resolved)) {
out.SetInt(int64(resolved))
- good = true
+ return true
}
case int64:
if !out.OverflowInt(resolved) {
out.SetInt(resolved)
- good = true
+ return true
}
case uint64:
if resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) {
out.SetInt(int64(resolved))
- good = true
+ return true
}
case float64:
if resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) {
out.SetInt(int64(resolved))
- good = true
+ return true
}
case string:
if out.Type() == durationType {
d, err := time.ParseDuration(resolved)
if err == nil {
out.SetInt(int64(d))
- good = true
+ return true
}
}
}
@@ -414,44 +471,49 @@
case int:
if resolved >= 0 && !out.OverflowUint(uint64(resolved)) {
out.SetUint(uint64(resolved))
- good = true
+ return true
}
case int64:
if resolved >= 0 && !out.OverflowUint(uint64(resolved)) {
out.SetUint(uint64(resolved))
- good = true
+ return true
}
case uint64:
if !out.OverflowUint(uint64(resolved)) {
out.SetUint(uint64(resolved))
- good = true
+ return true
}
case float64:
if resolved <= math.MaxUint64 && !out.OverflowUint(uint64(resolved)) {
out.SetUint(uint64(resolved))
- good = true
+ return true
}
}
case reflect.Bool:
switch resolved := resolved.(type) {
case bool:
out.SetBool(resolved)
- good = true
+ return true
}
case reflect.Float32, reflect.Float64:
switch resolved := resolved.(type) {
case int:
out.SetFloat(float64(resolved))
- good = true
+ return true
case int64:
out.SetFloat(float64(resolved))
- good = true
+ return true
case uint64:
out.SetFloat(float64(resolved))
- good = true
+ return true
case float64:
out.SetFloat(resolved)
- good = true
+ return true
+ }
+ case reflect.Struct:
+ if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() {
+ out.Set(resolvedv)
+ return true
}
case reflect.Ptr:
if out.Type().Elem() == reflect.TypeOf(resolved) {
@@ -459,13 +521,11 @@
elem := reflect.New(out.Type().Elem())
elem.Elem().Set(reflect.ValueOf(resolved))
out.Set(elem)
- good = true
+ return true
}
}
- if !good {
- d.terror(n, tag, out)
- }
- return good
+ d.terror(n, tag, out)
+ return false
}
func settableValueOf(i interface{}) reflect.Value {
@@ -561,7 +621,7 @@
}
e := reflect.New(et).Elem()
if d.unmarshal(n.children[i+1], e) {
- out.SetMapIndex(k, e)
+ d.setMapIndex(n.children[i+1], out, k, e)
}
}
}
@@ -569,6 +629,14 @@
return true
}
+func (d *decoder) setMapIndex(n *node, out, k, v reflect.Value) {
+ if d.strict && out.MapIndex(k) != zeroValue {
+ d.terrors = append(d.terrors, fmt.Sprintf("line %d: key %#v already set in map", n.line+1, k.Interface()))
+ return
+ }
+ out.SetMapIndex(k, v)
+}
+
func (d *decoder) mappingSlice(n *node, out reflect.Value) (good bool) {
outt := out.Type()
if outt.Elem() != mapItemType {
@@ -616,6 +684,10 @@
elemType = inlineMap.Type().Elem()
}
+ var doneFields []bool
+ if d.strict {
+ doneFields = make([]bool, len(sinfo.FieldsList))
+ }
for i := 0; i < l; i += 2 {
ni := n.children[i]
if isMerge(ni) {
@@ -626,6 +698,13 @@
continue
}
if info, ok := sinfo.FieldsMap[name.String()]; ok {
+ if d.strict {
+ if doneFields[info.Id] {
+ d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.line+1, name.String(), out.Type()))
+ continue
+ }
+ doneFields[info.Id] = true
+ }
var field reflect.Value
if info.Inline == nil {
field = out.Field(info.Num)
@@ -639,9 +718,9 @@
}
value := reflect.New(elemType).Elem()
d.unmarshal(n.children[i+1], value)
- inlineMap.SetMapIndex(name, value)
+ d.setMapIndex(n.children[i+1], inlineMap, name, value)
} else if d.strict {
- d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in struct %s", ni.line+1, name.String(), out.Type()))
+ d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.line+1, name.String(), out.Type()))
}
}
return true
diff --git a/decode_test.go b/decode_test.go
index e5366c2..7e1e8b3 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -2,13 +2,14 @@
import (
"errors"
- . "gopkg.in/check.v1"
- "gopkg.in/yaml.v2"
+ "io"
"math"
- "net"
"reflect"
"strings"
"time"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
)
var unmarshalIntTest = 123
@@ -19,8 +20,9 @@
}{
{
"",
- &struct{}{},
- }, {
+ (*struct{})(nil),
+ },
+ {
"{}", &struct{}{},
}, {
"v: hi",
@@ -425,13 +427,6 @@
}, {
"a: &a [1, 2]\nb: *a",
&struct{ B []int }{[]int{1, 2}},
- }, {
- "b: *a\na: &a {c: 1}",
- &struct {
- A, B struct {
- C int
- }
- }{struct{ C int }{1}, struct{ C int }{1}},
},
// Bug #1133337
@@ -517,6 +512,18 @@
map[string]interface{}{"a": "50cent_of_dollar"},
},
+ // issue #295 (allow scalars with colons in flow mappings and sequences)
+ {
+ "a: {b: https://github.com/go-yaml/yaml}",
+ map[string]interface{}{"a": map[interface{}]interface{}{
+ "b": "https://github.com/go-yaml/yaml",
+ }},
+ },
+ {
+ "a: [https://github.com/go-yaml/yaml]",
+ map[string]interface{}{"a": []interface{}{"https://github.com/go-yaml/yaml"}},
+ },
+
// Duration
{
"a: 3s",
@@ -568,11 +575,80 @@
// Support encoding.TextUnmarshaler.
{
"a: 1.2.3.4\n",
- map[string]net.IP{"a": net.IPv4(1, 2, 3, 4)},
+ map[string]textUnmarshaler{"a": textUnmarshaler{S: "1.2.3.4"}},
},
{
"a: 2015-02-24T18:19:39Z\n",
- map[string]time.Time{"a": time.Unix(1424801979, 0).In(time.UTC)},
+ map[string]textUnmarshaler{"a": textUnmarshaler{"2015-02-24T18:19:39Z"}},
+ },
+
+ // Timestamps
+ {
+ // Date only.
+ "a: 2015-01-01\n",
+ map[string]time.Time{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)},
+ },
+ {
+ // RFC3339
+ "a: 2015-02-24T18:19:39.12Z\n",
+ map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, .12e9, time.UTC)},
+ },
+ {
+ // RFC3339 with short dates.
+ "a: 2015-2-3T3:4:5Z",
+ map[string]time.Time{"a": time.Date(2015, 2, 3, 3, 4, 5, 0, time.UTC)},
+ },
+ {
+ // ISO8601 lower case t
+ "a: 2015-02-24t18:19:39Z\n",
+ map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC)},
+ },
+ {
+ // space separate, no time zone
+ "a: 2015-02-24 18:19:39\n",
+ map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC)},
+ },
+ // Some cases not currently handled. Uncomment these when
+ // the code is fixed.
+ // {
+ // // space separated with time zone
+ // "a: 2001-12-14 21:59:43.10 -5",
+ // map[string]interface{}{"a": time.Date(2001, 12, 14, 21, 59, 43, .1e9, time.UTC)},
+ // },
+ // {
+ // // arbitrary whitespace between fields
+ // "a: 2001-12-14 \t\t \t21:59:43.10 \t Z",
+ // map[string]interface{}{"a": time.Date(2001, 12, 14, 21, 59, 43, .1e9, time.UTC)},
+ // },
+ {
+ // explicit string tag
+ "a: !!str 2015-01-01",
+ map[string]interface{}{"a": "2015-01-01"},
+ },
+ {
+ // explicit timestamp tag on quoted string
+ "a: !!timestamp \"2015-01-01\"",
+ map[string]time.Time{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)},
+ },
+ {
+ // explicit timestamp tag on unquoted string
+ "a: !!timestamp 2015-01-01",
+ map[string]time.Time{"a": time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)},
+ },
+ {
+ // quoted string that's a valid timestamp
+ "a: \"2015-01-01\"",
+ map[string]interface{}{"a": "2015-01-01"},
+ },
+ {
+ // explicit timestamp tag into interface.
+ "a: !!timestamp \"2015-01-01\"",
+ map[string]interface{}{"a": "2015-01-01"},
+ },
+ {
+ // implicit timestamp tag into interface.
+ "a: 2015-01-01",
+ map[string]interface{}{"a": "2015-01-01"},
},
// Encode empty lists as zero-length slices.
@@ -611,6 +687,21 @@
"a: 123456E1\n",
M{"a": "123456E1"},
},
+ // yaml-test-suite 3GZX: Spec Example 7.1. Alias Nodes
+ {
+ "First occurrence: &anchor Foo\nSecond occurrence: *anchor\nOverride anchor: &anchor Bar\nReuse anchor: *anchor\n",
+ map[interface{}]interface{}{
+ "Reuse anchor": "Bar",
+ "First occurrence": "Foo",
+ "Second occurrence": "Foo",
+ "Override anchor": "Bar",
+ },
+ },
+ // Single document with garbage following it.
+ {
+ "---\nhello\n...\n}not yaml",
+ "hello",
+ },
}
type M map[interface{}]interface{}
@@ -628,29 +719,87 @@
for i, item := range unmarshalTests {
c.Logf("test %d: %q", i, item.data)
t := reflect.ValueOf(item.value).Type()
- var value interface{}
- switch t.Kind() {
- case reflect.Map:
- value = reflect.MakeMap(t).Interface()
- case reflect.String:
- value = reflect.New(t).Interface()
- case reflect.Ptr:
- value = reflect.New(t.Elem()).Interface()
- default:
- c.Fatalf("missing case for %s", t)
- }
- err := yaml.Unmarshal([]byte(item.data), value)
+ value := reflect.New(t)
+ err := yaml.Unmarshal([]byte(item.data), value.Interface())
if _, ok := err.(*yaml.TypeError); !ok {
c.Assert(err, IsNil)
}
- if t.Kind() == reflect.String {
- c.Assert(*value.(*string), Equals, item.value)
- } else {
- c.Assert(value, DeepEquals, item.value)
- }
+ c.Assert(value.Elem().Interface(), DeepEquals, item.value, Commentf("error: %v", err))
}
}
+func (s *S) TestDecoderSingleDocument(c *C) {
+ // Test that Decoder.Decode works as expected on
+ // all the unmarshal tests.
+ for i, item := range unmarshalTests {
+ c.Logf("test %d: %q", i, item.data)
+ if item.data == "" {
+ // Behaviour differs when there's no YAML.
+ continue
+ }
+ t := reflect.ValueOf(item.value).Type()
+ value := reflect.New(t)
+ err := yaml.NewDecoder(strings.NewReader(item.data)).Decode(value.Interface())
+ if _, ok := err.(*yaml.TypeError); !ok {
+ c.Assert(err, IsNil)
+ }
+ c.Assert(value.Elem().Interface(), DeepEquals, item.value)
+ }
+}
+
+var decoderTests = []struct {
+ data string
+ values []interface{}
+}{{
+ "",
+ nil,
+}, {
+ "a: b",
+ []interface{}{
+ map[interface{}]interface{}{"a": "b"},
+ },
+}, {
+ "---\na: b\n...\n",
+ []interface{}{
+ map[interface{}]interface{}{"a": "b"},
+ },
+}, {
+ "---\n'hello'\n...\n---\ngoodbye\n...\n",
+ []interface{}{
+ "hello",
+ "goodbye",
+ },
+}}
+
+func (s *S) TestDecoder(c *C) {
+ for i, item := range decoderTests {
+ c.Logf("test %d: %q", i, item.data)
+ var values []interface{}
+ dec := yaml.NewDecoder(strings.NewReader(item.data))
+ for {
+ var value interface{}
+ err := dec.Decode(&value)
+ if err == io.EOF {
+ break
+ }
+ c.Assert(err, IsNil)
+ values = append(values, value)
+ }
+ c.Assert(values, DeepEquals, item.values)
+ }
+}
+
+type errReader struct{}
+
+func (errReader) Read([]byte) (int, error) {
+ return 0, errors.New("some read error")
+}
+
+func (s *S) TestDecoderReadError(c *C) {
+ err := yaml.NewDecoder(errReader{}).Decode(&struct{}{})
+ c.Assert(err, ErrorMatches, `yaml: input error: some read error`)
+}
+
func (s *S) TestUnmarshalNaN(c *C) {
value := map[string]interface{}{}
err := yaml.Unmarshal([]byte("notanum: .NaN"), &value)
@@ -670,17 +819,27 @@
{"a: !!binary ==", "yaml: !!binary value contains invalid base64 data"},
{"{[.]}", `yaml: invalid map key: \[\]interface \{\}\{"\."\}`},
{"{{.}}", `yaml: invalid map key: map\[interface\ \{\}\]interface \{\}\{".":interface \{\}\(nil\)\}`},
+ {"b: *a\na: &a {c: 1}", `yaml: unknown anchor 'a' referenced`},
{"%TAG !%79! tag:yaml.org,2002:\n---\nv: !%79!int '1'", "yaml: did not find expected whitespace"},
}
func (s *S) TestUnmarshalErrors(c *C) {
- for _, item := range unmarshalErrorTests {
+ for i, item := range unmarshalErrorTests {
+ c.Logf("test %d: %q", i, item.data)
var value interface{}
err := yaml.Unmarshal([]byte(item.data), &value)
c.Assert(err, ErrorMatches, item.error, Commentf("Partial unmarshal: %#v", value))
}
}
+func (s *S) TestDecoderErrors(c *C) {
+ for _, item := range unmarshalErrorTests {
+ var value interface{}
+ err := yaml.NewDecoder(strings.NewReader(item.data)).Decode(&value)
+ c.Assert(err, ErrorMatches, item.error, Commentf("Partial unmarshal: %#v", value))
+ }
+}
+
var unmarshalerTests = []struct {
data, tag string
value interface{}
@@ -991,15 +1150,96 @@
c.Assert(v.A, DeepEquals, []int{2})
}
-func (s *S) TestUnmarshalStrict(c *C) {
- v := struct{ A, B int }{}
+var unmarshalStrictTests = []struct {
+ data string
+ value interface{}
+ error string
+}{{
+ data: "a: 1\nc: 2\n",
+ value: struct{ A, B int }{A: 1},
+ error: `yaml: unmarshal errors:\n line 2: field c not found in type struct { A int; B int }`,
+}, {
+ data: "a: 1\nb: 2\na: 3\n",
+ value: struct{ A, B int }{A: 3, B: 2},
+ error: `yaml: unmarshal errors:\n line 3: field a already set in type struct { A int; B int }`,
+}, {
+ data: "c: 3\na: 1\nb: 2\nc: 4\n",
+ value: struct {
+ A int
+ inlineB `yaml:",inline"`
+ }{
+ A: 1,
+ inlineB: inlineB{
+ B: 2,
+ inlineC: inlineC{
+ C: 4,
+ },
+ },
+ },
+ error: `yaml: unmarshal errors:\n line 4: field c already set in type struct { A int; yaml_test.inlineB "yaml:\\",inline\\"" }`,
+}, {
+ data: "c: 0\na: 1\nb: 2\nc: 1\n",
+ value: struct {
+ A int
+ inlineB `yaml:",inline"`
+ }{
+ A: 1,
+ inlineB: inlineB{
+ B: 2,
+ inlineC: inlineC{
+ C: 1,
+ },
+ },
+ },
+ error: `yaml: unmarshal errors:\n line 4: field c already set in type struct { A int; yaml_test.inlineB "yaml:\\",inline\\"" }`,
+}, {
+ data: "c: 1\na: 1\nb: 2\nc: 3\n",
+ value: struct {
+ A int
+ M map[string]interface{} `yaml:",inline"`
+ }{
+ A: 1,
+ M: map[string]interface{}{
+ "b": 2,
+ "c": 3,
+ },
+ },
+ error: `yaml: unmarshal errors:\n line 4: key "c" already set in map`,
+}, {
+ data: "a: 1\n9: 2\nnull: 3\n9: 4",
+ value: map[interface{}]interface{}{
+ "a": 1,
+ nil: 3,
+ 9: 4,
+ },
+ error: `yaml: unmarshal errors:\n line 4: key 9 already set in map`,
+}}
- err := yaml.UnmarshalStrict([]byte("a: 1\nb: 2"), &v)
- c.Check(err, IsNil)
- err = yaml.Unmarshal([]byte("a: 1\nb: 2\nc: 3"), &v)
- c.Check(err, IsNil)
- err = yaml.UnmarshalStrict([]byte("a: 1\nb: 2\nc: 3"), &v)
- c.Check(err, ErrorMatches, "yaml: unmarshal errors:\n line 3: field c not found in struct struct { A int; B int }")
+func (s *S) TestUnmarshalStrict(c *C) {
+ for i, item := range unmarshalStrictTests {
+ c.Logf("test %d: %q", i, item.data)
+ // First test that normal Unmarshal unmarshals to the expected value.
+ t := reflect.ValueOf(item.value).Type()
+ value := reflect.New(t)
+ err := yaml.Unmarshal([]byte(item.data), value.Interface())
+ c.Assert(err, Equals, nil)
+ c.Assert(value.Elem().Interface(), DeepEquals, item.value)
+
+ // Then test that UnmarshalStrict fails on the same thing.
+ t = reflect.ValueOf(item.value).Type()
+ value = reflect.New(t)
+ err = yaml.UnmarshalStrict([]byte(item.data), value.Interface())
+ c.Assert(err, ErrorMatches, item.error)
+ }
+}
+
+type textUnmarshaler struct {
+ S string
+}
+
+func (t *textUnmarshaler) UnmarshalText(s []byte) error {
+ t.S = string(s)
+ return nil
}
//var data []byte
diff --git a/emitterc.go b/emitterc.go
index dcaf502..cf0db11 100644
--- a/emitterc.go
+++ b/emitterc.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ "fmt"
)
// Flush the buffer if needed.
@@ -664,7 +665,7 @@
return yaml_emitter_emit_mapping_start(emitter, event)
default:
return yaml_emitter_set_emitter_error(emitter,
- "expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS")
+ fmt.Sprintf("expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, but got %v", event.typ))
}
}
diff --git a/encode.go b/encode.go
index 84f8499..1e730ef 100644
--- a/encode.go
+++ b/encode.go
@@ -3,12 +3,14 @@
import (
"encoding"
"fmt"
+ "io"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"
+ "unicode/utf8"
)
type encoder struct {
@@ -16,25 +18,39 @@
event yaml_event_t
out []byte
flow bool
+ // doneInit holds whether the initial stream_start_event has been
+ // emitted.
+ doneInit bool
}
-func newEncoder() (e *encoder) {
- e = &encoder{}
- e.must(yaml_emitter_initialize(&e.emitter))
+func newEncoder() *encoder {
+ e := &encoder{}
+ yaml_emitter_initialize(&e.emitter)
yaml_emitter_set_output_string(&e.emitter, &e.out)
yaml_emitter_set_unicode(&e.emitter, true)
- e.must(yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING))
- e.emit()
- e.must(yaml_document_start_event_initialize(&e.event, nil, nil, true))
- e.emit()
return e
}
-func (e *encoder) finish() {
- e.must(yaml_document_end_event_initialize(&e.event, true))
+func newEncoderWithWriter(w io.Writer) *encoder {
+ e := &encoder{}
+ yaml_emitter_initialize(&e.emitter)
+ yaml_emitter_set_output_writer(&e.emitter, w)
+ yaml_emitter_set_unicode(&e.emitter, true)
+ return e
+}
+
+func (e *encoder) init() {
+ if e.doneInit {
+ return
+ }
+ yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING)
e.emit()
+ e.doneInit = true
+}
+
+func (e *encoder) finish() {
e.emitter.open_ended = false
- e.must(yaml_stream_end_event_initialize(&e.event))
+ yaml_stream_end_event_initialize(&e.event)
e.emit()
}
@@ -44,9 +60,7 @@
func (e *encoder) emit() {
// This will internally delete the e.event value.
- if !yaml_emitter_emit(&e.emitter, &e.event) && e.event.typ != yaml_DOCUMENT_END_EVENT && e.event.typ != yaml_STREAM_END_EVENT {
- e.must(false)
- }
+ e.must(yaml_emitter_emit(&e.emitter, &e.event))
}
func (e *encoder) must(ok bool) {
@@ -59,13 +73,28 @@
}
}
+func (e *encoder) marshalDoc(tag string, in reflect.Value) {
+ e.init()
+ yaml_document_start_event_initialize(&e.event, nil, nil, true)
+ e.emit()
+ e.marshal(tag, in)
+ yaml_document_end_event_initialize(&e.event, true)
+ e.emit()
+}
+
func (e *encoder) marshal(tag string, in reflect.Value) {
- if !in.IsValid() {
+ if !in.IsValid() || in.Kind() == reflect.Ptr && in.IsNil() {
e.nilv()
return
}
iface := in.Interface()
- if m, ok := iface.(Marshaler); ok {
+ switch m := iface.(type) {
+ case time.Time, *time.Time:
+ // Although time.Time implements TextMarshaler,
+ // we don't want to treat it as a string for YAML
+ // purposes because YAML has special support for
+ // timestamps.
+ case Marshaler:
v, err := m.MarshalYAML()
if err != nil {
fail(err)
@@ -75,30 +104,33 @@
return
}
in = reflect.ValueOf(v)
- } else if m, ok := iface.(encoding.TextMarshaler); ok {
+ case encoding.TextMarshaler:
text, err := m.MarshalText()
if err != nil {
fail(err)
}
in = reflect.ValueOf(string(text))
+ case nil:
+ e.nilv()
+ return
}
switch in.Kind() {
case reflect.Interface:
- if in.IsNil() {
- e.nilv()
- } else {
- e.marshal(tag, in.Elem())
- }
+ e.marshal(tag, in.Elem())
case reflect.Map:
e.mapv(tag, in)
case reflect.Ptr:
- if in.IsNil() {
- e.nilv()
+ if in.Type() == ptrTimeType {
+ e.timev(tag, in.Elem())
} else {
e.marshal(tag, in.Elem())
}
case reflect.Struct:
- e.structv(tag, in)
+ if in.Type() == timeType {
+ e.timev(tag, in)
+ } else {
+ e.structv(tag, in)
+ }
case reflect.Slice:
if in.Type().Elem() == mapItemType {
e.itemsv(tag, in)
@@ -191,10 +223,10 @@
e.flow = false
style = yaml_FLOW_MAPPING_STYLE
}
- e.must(yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style))
+ yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style)
e.emit()
f()
- e.must(yaml_mapping_end_event_initialize(&e.event))
+ yaml_mapping_end_event_initialize(&e.event)
e.emit()
}
@@ -240,23 +272,36 @@
func (e *encoder) stringv(tag string, in reflect.Value) {
var style yaml_scalar_style_t
s := in.String()
- rtag, rs := resolve("", s)
- if rtag == yaml_BINARY_TAG {
- if tag == "" || tag == yaml_STR_TAG {
- tag = rtag
- s = rs.(string)
- } else if tag == yaml_BINARY_TAG {
+ canUsePlain := true
+ switch {
+ case !utf8.ValidString(s):
+ if tag == yaml_BINARY_TAG {
failf("explicitly tagged !!binary data must be base64-encoded")
- } else {
+ }
+ if tag != "" {
failf("cannot marshal invalid UTF-8 data as %s", shortTag(tag))
}
+ // It can't be encoded directly as YAML so use a binary tag
+ // and encode it as base64.
+ tag = yaml_BINARY_TAG
+ s = encodeBase64(s)
+ case tag == "":
+ // Check to see if it would resolve to a specific
+ // tag when encoded unquoted. If it doesn't,
+ // there's no need to quote it.
+ rtag, _ := resolve("", s)
+ canUsePlain = rtag == yaml_STR_TAG && !isBase60Float(s)
}
- if tag == "" && (rtag != yaml_STR_TAG || isBase60Float(s)) {
- style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
- } else if strings.Contains(s, "\n") {
+ // Note: it's possible for user code to emit invalid YAML
+ // if they explicitly specify a tag and a string containing
+ // text that's incompatible with that tag.
+ switch {
+ case strings.Contains(s, "\n"):
style = yaml_LITERAL_SCALAR_STYLE
- } else {
+ case canUsePlain:
style = yaml_PLAIN_SCALAR_STYLE
+ default:
+ style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
}
e.emitScalar(s, "", tag, style)
}
@@ -281,9 +326,16 @@
e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE)
}
+func (e *encoder) timev(tag string, in reflect.Value) {
+ t := in.Interface().(time.Time)
+ if tag == "" {
+ tag = yaml_TIMESTAMP_TAG
+ }
+ e.emitScalar(t.Format(time.RFC3339Nano), "", tag, yaml_PLAIN_SCALAR_STYLE)
+}
+
func (e *encoder) floatv(tag string, in reflect.Value) {
- // FIXME: Handle 64 bits here.
- s := strconv.FormatFloat(float64(in.Float()), 'g', -1, 32)
+ s := strconv.FormatFloat(in.Float(), 'g', -1, 64)
switch s {
case "+Inf":
s = ".inf"
diff --git a/encode_test.go b/encode_test.go
index 84099bd..2e78411 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -1,16 +1,18 @@
package yaml_test
import (
+ "bytes"
"fmt"
"math"
"strconv"
"strings"
"time"
- . "gopkg.in/check.v1"
- "gopkg.in/yaml.v2"
"net"
"os"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
)
var marshalIntTest = 123
@@ -23,6 +25,9 @@
nil,
"null\n",
}, {
+ (*marshalerType)(nil),
+ "null\n",
+ }, {
&struct{}{},
"{}\n",
}, {
@@ -197,6 +202,25 @@
}{1, 0},
"a: 1\n",
},
+ {
+ &struct {
+ T1 time.Time "t1,omitempty"
+ T2 time.Time "t2,omitempty"
+ T3 *time.Time "t3,omitempty"
+ T4 *time.Time "t4,omitempty"
+ }{
+ T2: time.Date(2018, 1, 9, 10, 40, 47, 0, time.UTC),
+ T4: newTime(time.Date(2098, 1, 9, 10, 40, 47, 0, time.UTC)),
+ },
+ "t2: !!timestamp 2018-01-09T10:40:47Z\nt4: !!timestamp 2098-01-09T10:40:47Z\n",
+ },
+ // Nil interface that implements Marshaler.
+ {
+ map[string]yaml.Marshaler{
+ "a": nil,
+ },
+ "a: null\n",
+ },
// Flow flag
{
@@ -302,9 +326,19 @@
map[string]net.IP{"a": net.IPv4(1, 2, 3, 4)},
"a: 1.2.3.4\n",
},
+ // time.Time gets a timestamp tag.
{
- map[string]time.Time{"a": time.Unix(1424801979, 0)},
- "a: 2015-02-24T18:19:39Z\n",
+ map[string]time.Time{"a": time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC)},
+ "a: !!timestamp 2015-02-24T18:19:39Z\n",
+ },
+ {
+ map[string]*time.Time{"a": newTime(time.Date(2015, 2, 24, 18, 19, 39, 0, time.UTC))},
+ "a: !!timestamp 2015-02-24T18:19:39Z\n",
+ },
+ // Ensure timestamp-like strings are quoted.
+ {
+ map[string]string{"a": "2015-02-24T18:19:39Z"},
+ "a: \"2015-02-24T18:19:39Z\"\n",
},
// Ensure strings containing ": " are quoted (reported as PR #43, but not reproducible).
@@ -327,13 +361,51 @@
func (s *S) TestMarshal(c *C) {
defer os.Setenv("TZ", os.Getenv("TZ"))
os.Setenv("TZ", "UTC")
- for _, item := range marshalTests {
+ for i, item := range marshalTests {
+ c.Logf("test %d: %q", i, item.data)
data, err := yaml.Marshal(item.value)
c.Assert(err, IsNil)
c.Assert(string(data), Equals, item.data)
}
}
+func (s *S) TestEncoderSingleDocument(c *C) {
+ for i, item := range marshalTests {
+ c.Logf("test %d. %q", i, item.data)
+ var buf bytes.Buffer
+ enc := yaml.NewEncoder(&buf)
+ err := enc.Encode(item.value)
+ c.Assert(err, Equals, nil)
+ err = enc.Close()
+ c.Assert(err, Equals, nil)
+ c.Assert(buf.String(), Equals, item.data)
+ }
+}
+
+func (s *S) TestEncoderMultipleDocuments(c *C) {
+ var buf bytes.Buffer
+ enc := yaml.NewEncoder(&buf)
+ err := enc.Encode(map[string]string{"a": "b"})
+ c.Assert(err, Equals, nil)
+ err = enc.Encode(map[string]string{"c": "d"})
+ c.Assert(err, Equals, nil)
+ err = enc.Close()
+ c.Assert(err, Equals, nil)
+ c.Assert(buf.String(), Equals, "a: b\n---\nc: d\n")
+}
+
+func (s *S) TestEncoderWriteError(c *C) {
+ enc := yaml.NewEncoder(errorWriter{})
+ err := enc.Encode(map[string]string{"a": "b"})
+ c.Assert(err, ErrorMatches, `yaml: write error: some write error`) // Data not flushed yet
+}
+
+type errorWriter struct{}
+
+func (errorWriter) Write([]byte) (int, error) {
+ return 0, fmt.Errorf("some write error")
+}
+
var marshalErrorTests = []struct {
value interface{}
error string
@@ -499,3 +571,7 @@
last = index
}
}
+
+func newTime(t time.Time) *time.Time {
+ return &t
+}
diff --git a/resolve.go b/resolve.go
index 232313c..ea90bd5 100644
--- a/resolve.go
+++ b/resolve.go
@@ -6,7 +6,7 @@
"regexp"
"strconv"
"strings"
- "unicode/utf8"
+ "time"
)
type resolveMapItem struct {
@@ -75,7 +75,7 @@
func resolvableTag(tag string) bool {
switch tag {
- case "", yaml_STR_TAG, yaml_BOOL_TAG, yaml_INT_TAG, yaml_FLOAT_TAG, yaml_NULL_TAG:
+ case "", yaml_STR_TAG, yaml_BOOL_TAG, yaml_INT_TAG, yaml_FLOAT_TAG, yaml_NULL_TAG, yaml_TIMESTAMP_TAG:
return true
}
return false
@@ -125,6 +125,15 @@
case 'D', 'S':
// Int, float, or timestamp.
+ // Only try values as a timestamp if the value is unquoted or there's an explicit
+ // !!timestamp tag.
+ if tag == "" || tag == yaml_TIMESTAMP_TAG {
+ t, ok := parseTimestamp(in)
+ if ok {
+ return yaml_TIMESTAMP_TAG, t
+ }
+ }
+
plain := strings.Replace(in, "_", "", -1)
intv, err := strconv.ParseInt(plain, 0, 64)
if err == nil {
@@ -167,19 +176,11 @@
}
}
}
- // XXX Handle timestamps here.
-
default:
panic("resolveTable item not yet handled: " + string(rune(hint)) + " (with " + in + ")")
}
}
- if tag == yaml_BINARY_TAG {
- return yaml_BINARY_TAG, in
- }
- if utf8.ValidString(in) {
- return yaml_STR_TAG, in
- }
- return yaml_BINARY_TAG, encodeBase64(in)
+ return yaml_STR_TAG, in
}
// encodeBase64 encodes s as base64 that is broken up into multiple lines
@@ -206,3 +207,39 @@
}
return string(out[:k])
}
+
+// This is a subset of the formats allowed by the regular expression
+// defined at http://yaml.org/type/timestamp.html.
+var allowedTimestampFormats = []string{
+ "2006-1-2T15:4:5Z07:00",
+ "2006-1-2t15:4:5Z07:00", // RFC3339 with lower-case "t".
+ "2006-1-2 15:4:5", // space separated with no time zone
+ "2006-1-2", // date only
+ // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5"
+ // from the set of examples.
+}
+
+// parseTimestamp parses s as a timestamp string and
+// returns the timestamp and reports whether it succeeded.
+// Timestamp formats are defined at http://yaml.org/type/timestamp.html
+func parseTimestamp(s string) (time.Time, bool) {
+ // TODO write code to check all the formats supported by
+ // http://yaml.org/type/timestamp.html instead of using time.Parse.
+
+ // Quick check: all date formats start with YYYY-.
+ i := 0
+ for ; i < len(s); i++ {
+ if c := s[i]; c < '0' || c > '9' {
+ break
+ }
+ }
+ if i != 4 || i == len(s) || s[i] != '-' {
+ return time.Time{}, false
+ }
+ for _, format := range allowedTimestampFormats {
+ if t, err := time.Parse(format, s); err == nil {
+ return t, true
+ }
+ }
+ return time.Time{}, false
+}
diff --git a/scannerc.go b/scannerc.go
index 0744844..492a984 100644
--- a/scannerc.go
+++ b/scannerc.go
@@ -2592,19 +2592,10 @@
// Consume non-blank characters.
for !is_blankz(parser.buffer, parser.buffer_pos) {
- // Check for 'x:x' in the flow context. TODO: Fix the test "spec-08-13".
- if parser.flow_level > 0 &&
- parser.buffer[parser.buffer_pos] == ':' &&
- !is_blankz(parser.buffer, parser.buffer_pos+1) {
- yaml_parser_set_scanner_error(parser, "while scanning a plain scalar",
- start_mark, "found unexpected ':'")
- return false
- }
-
// Check for indicators that may end a plain scalar.
if (parser.buffer[parser.buffer_pos] == ':' && is_blankz(parser.buffer, parser.buffer_pos+1)) ||
(parser.flow_level > 0 &&
- (parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == ':' ||
+ (parser.buffer[parser.buffer_pos] == ',' ||
parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == '[' ||
parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' ||
parser.buffer[parser.buffer_pos] == '}')) {
diff --git a/writerc.go b/writerc.go
index 190362f..a2dde60 100644
--- a/writerc.go
+++ b/writerc.go
@@ -18,72 +18,9 @@
return true
}
- // If the output encoding is UTF-8, we don't need to recode the buffer.
- if emitter.encoding == yaml_UTF8_ENCODING {
- if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil {
- return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error())
- }
- emitter.buffer_pos = 0
- return true
- }
-
- // Recode the buffer into the raw buffer.
- var low, high int
- if emitter.encoding == yaml_UTF16LE_ENCODING {
- low, high = 0, 1
- } else {
- high, low = 1, 0
- }
-
- pos := 0
- for pos < emitter.buffer_pos {
- // See the "reader.c" code for more details on UTF-8 encoding. Note
- // that we assume that the buffer contains a valid UTF-8 sequence.
-
- // Read the next UTF-8 character.
- octet := emitter.buffer[pos]
-
- var w int
- var value rune
- switch {
- case octet&0x80 == 0x00:
- w, value = 1, rune(octet&0x7F)
- case octet&0xE0 == 0xC0:
- w, value = 2, rune(octet&0x1F)
- case octet&0xF0 == 0xE0:
- w, value = 3, rune(octet&0x0F)
- case octet&0xF8 == 0xF0:
- w, value = 4, rune(octet&0x07)
- }
- for k := 1; k < w; k++ {
- octet = emitter.buffer[pos+k]
- value = (value << 6) + (rune(octet) & 0x3F)
- }
- pos += w
-
- // Write the character.
- if value < 0x10000 {
- var b [2]byte
- b[high] = byte(value >> 8)
- b[low] = byte(value & 0xFF)
- emitter.raw_buffer = append(emitter.raw_buffer, b[0], b[1])
- } else {
- // Write the character using a surrogate pair (check "reader.c").
- var b [4]byte
- value -= 0x10000
- b[high] = byte(0xD8 + (value >> 18))
- b[low] = byte((value >> 10) & 0xFF)
- b[high+2] = byte(0xDC + ((value >> 8) & 0xFF))
- b[low+2] = byte(value & 0xFF)
- emitter.raw_buffer = append(emitter.raw_buffer, b[0], b[1], b[2], b[3])
- }
- }
-
- // Write the raw buffer.
- if err := emitter.write_handler(emitter, emitter.raw_buffer); err != nil {
+ if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil {
return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error())
}
emitter.buffer_pos = 0
- emitter.raw_buffer = emitter.raw_buffer[:0]
return true
}
diff --git a/yaml.go b/yaml.go
index 5e3c2da..483aae5 100644
--- a/yaml.go
+++ b/yaml.go
@@ -9,6 +9,7 @@
import (
"errors"
"fmt"
+ "io"
"reflect"
"strings"
"sync"
@@ -81,12 +82,58 @@
}
// UnmarshalStrict is like Unmarshal except that any fields that are found
-// in the data that do not have corresponding struct members will result in
+// in the data that do not have corresponding struct members, or mapping
+// keys that are duplicates, will result in
// an error.
func UnmarshalStrict(in []byte, out interface{}) (err error) {
return unmarshal(in, out, true)
}
+// A Decorder reads and decodes YAML values from an input stream.
+type Decoder struct {
+ strict bool
+ parser *parser
+}
+
+// NewDecoder returns a new decoder that reads from r.
+//
+// The decoder introduces its own buffering and may read
+// data from r beyond the YAML values requested.
+func NewDecoder(r io.Reader) *Decoder {
+ return &Decoder{
+ parser: newParserFromReader(r),
+ }
+}
+
+// SetStrict sets whether strict decoding behaviour is enabled when
+// decoding items in the data (see UnmarshalStrict). By default, decoding is not strict.
+func (dec *Decoder) SetStrict(strict bool) {
+ dec.strict = strict
+}
+
+// Decode reads the next YAML-encoded value from its input
+// and stores it in the value pointed to by v.
+//
+// See the documentation for Unmarshal for details about the
+// conversion of YAML into a Go value.
+func (dec *Decoder) Decode(v interface{}) (err error) {
+ d := newDecoder(dec.strict)
+ defer handleErr(&err)
+ node := dec.parser.parse()
+ if node == nil {
+ return io.EOF
+ }
+ out := reflect.ValueOf(v)
+ if out.Kind() == reflect.Ptr && !out.IsNil() {
+ out = out.Elem()
+ }
+ d.unmarshal(node, out)
+ if len(d.terrors) > 0 {
+ return &TypeError{d.terrors}
+ }
+ return nil
+}
+
func unmarshal(in []byte, out interface{}, strict bool) (err error) {
defer handleErr(&err)
d := newDecoder(strict)
@@ -125,7 +172,10 @@
//
// omitempty Only include the field if it's not set to the zero
// value for the type or to empty slices or maps.
-// Does not apply to zero valued structs.
+// Zero valued structs will be omitted if all their public
+// fields are zero, unless they implement an IsZero
+// method (see the IsZeroer interface type), in which
+// case the field will be included if that method returns true.
//
// flow Marshal using a flow style (useful for structs,
// sequences and maps).
@@ -150,12 +200,47 @@
defer handleErr(&err)
e := newEncoder()
defer e.destroy()
- e.marshal("", reflect.ValueOf(in))
+ e.marshalDoc("", reflect.ValueOf(in))
e.finish()
out = e.out
return
}
+// An Encoder writes YAML values to an output stream.
+type Encoder struct {
+ encoder *encoder
+}
+
+// NewEncoder returns a new encoder that writes to w.
+// The Encoder should be closed after use to flush all data
+// to w.
+func NewEncoder(w io.Writer) *Encoder {
+ return &Encoder{
+ encoder: newEncoderWithWriter(w),
+ }
+}
+
+// Encode writes the YAML encoding of v to the stream.
+// If multiple items are encoded to the stream, the
+// second and subsequent document will be preceded
+// with a "---" document separator, but the first will not.
+//
+// See the documentation for Marshal for details about the conversion of Go
+// values to YAML.
+func (e *Encoder) Encode(v interface{}) (err error) {
+ defer handleErr(&err)
+ e.encoder.marshalDoc("", reflect.ValueOf(v))
+ return nil
+}
+
+// Close closes the encoder by writing any remaining data.
+// It does not write a stream terminating string "...".
+func (e *Encoder) Close() (err error) {
+ defer handleErr(&err)
+ e.encoder.finish()
+ return nil
+}
+
func handleErr(err *error) {
if v := recover(); v != nil {
if e, ok := v.(yamlError); ok {
@@ -211,6 +296,9 @@
Num int
OmitEmpty bool
Flow bool
+ // Id holds the unique field identifier, so we can cheaply
+ // check for field duplicates without maintaining an extra map.
+ Id int
// Inline holds the field index if the field is part of an inlined struct.
Inline []int
@@ -290,6 +378,7 @@
} else {
finfo.Inline = append([]int{i}, finfo.Inline...)
}
+ finfo.Id = len(fieldsList)
fieldsMap[finfo.Key] = finfo
fieldsList = append(fieldsList, finfo)
}
@@ -311,11 +400,16 @@
return nil, errors.New(msg)
}
+ info.Id = len(fieldsList)
fieldsList = append(fieldsList, info)
fieldsMap[info.Key] = info
}
- sinfo = &structInfo{fieldsMap, fieldsList, inlineMap}
+ sinfo = &structInfo{
+ FieldsMap: fieldsMap,
+ FieldsList: fieldsList,
+ InlineMap: inlineMap,
+ }
fieldMapMutex.Lock()
structMap[st] = sinfo
@@ -323,8 +417,23 @@
return sinfo, nil
}
+// IsZeroer is used to check whether an object is zero to
+// determine whether it should be omitted when marshaling
+// with the omitempty flag. One notable implementation
+// is time.Time.
+type IsZeroer interface {
+ IsZero() bool
+}
+
func isZero(v reflect.Value) bool {
- switch v.Kind() {
+ kind := v.Kind()
+ if z, ok := v.Interface().(IsZeroer); ok {
+ if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() {
+ return true
+ }
+ return z.IsZero()
+ }
+ switch kind {
case reflect.String:
return len(v.String()) == 0
case reflect.Interface, reflect.Ptr:
diff --git a/yamlh.go b/yamlh.go
index 3caeca0..e25cee5 100644
--- a/yamlh.go
+++ b/yamlh.go
@@ -1,6 +1,7 @@
package yaml
import (
+ "fmt"
"io"
)
@@ -239,6 +240,27 @@
yaml_MAPPING_END_EVENT // A MAPPING-END event.
)
+var eventStrings = []string{
+ yaml_NO_EVENT: "none",
+ yaml_STREAM_START_EVENT: "stream start",
+ yaml_STREAM_END_EVENT: "stream end",
+ yaml_DOCUMENT_START_EVENT: "document start",
+ yaml_DOCUMENT_END_EVENT: "document end",
+ yaml_ALIAS_EVENT: "alias",
+ yaml_SCALAR_EVENT: "scalar",
+ yaml_SEQUENCE_START_EVENT: "sequence start",
+ yaml_SEQUENCE_END_EVENT: "sequence end",
+ yaml_MAPPING_START_EVENT: "mapping start",
+ yaml_MAPPING_END_EVENT: "mapping end",
+}
+
+func (e yaml_event_type_t) String() string {
+ if e < 0 || int(e) >= len(eventStrings) {
+ return fmt.Sprintf("unknown event %d", e)
+ }
+ return eventStrings[e]
+}
+
// The event structure.
type yaml_event_t struct {
@@ -521,9 +543,9 @@
read_handler yaml_read_handler_t // Read handler.
- input_file io.Reader // File input data.
- input []byte // String input data.
- input_pos int
+ input_reader io.Reader // File input data.
+ input []byte // String input data.
+ input_pos int
eof bool // EOF flag
@@ -632,7 +654,7 @@
write_handler yaml_write_handler_t // Write handler.
output_buffer *[]byte // String output data.
- output_file io.Writer // File output data.
+ output_writer io.Writer // File output data.
buffer []byte // The working buffer.
buffer_pos int // The current position of the buffer.