diff --git a/CHANGELOG.md b/CHANGELOG.md index ecac81a7..6af653eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Resolved an issue with YAML parser that was causing strings to be read as numbers. +- Timestamps can now be resolved as expected in YAML. ## [v2.3.4] - 2023-06-01 diff --git a/dencoding/yaml.go b/dencoding/yaml.go index 7c36a4ca..7a62e512 100644 --- a/dencoding/yaml.go +++ b/dencoding/yaml.go @@ -1,14 +1,15 @@ package dencoding const ( - yamlTagString = "!!str" - yamlTagMap = "!!map" - yamlTagArray = "!!seq" - yamlTagNull = "!!null" - yamlTagBinary = "!!binary" - yamlTagBool = "!!bool" - yamlTagInt = "!!int" - yamlTagFloat = "!!float" + yamlTagString = "!!str" + yamlTagMap = "!!map" + yamlTagArray = "!!seq" + yamlTagNull = "!!null" + yamlTagBinary = "!!binary" + yamlTagBool = "!!bool" + yamlTagInt = "!!int" + yamlTagFloat = "!!float" + yamlTagTimestamp = "!!timestamp" ) // YAMLEncoderOption is identifies an option that can be applied to a YAML encoder. diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go index 038a38fb..48e04c36 100644 --- a/dencoding/yaml_decoder.go +++ b/dencoding/yaml_decoder.go @@ -7,6 +7,7 @@ import ( "io" "reflect" "strconv" + "time" ) // YAMLDecoder wraps a standard yaml encoder to implement custom ordering logic. @@ -116,6 +117,12 @@ func (decoder *YAMLDecoder) getScalarNodeValue(node *yaml.Node) (any, error) { return strconv.ParseInt(node.Value, 0, 64) case yamlTagString: return node.Value, nil + case yamlTagTimestamp: + value, ok := parseTimestamp(node.Value) + if !ok { + return value, fmt.Errorf("could not parse timestamp: %v", node.Value) + } + return value, nil default: return nil, fmt.Errorf("unhandled scalar node tag: %v", node.ShortTag()) } @@ -128,3 +135,40 @@ func (decoder *YAMLDecoder) nextNode() (*yaml.Node, error) { } return &node, nil } + +// 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:5.999999999Z07:00", // RCF3339Nano with short date fields. + "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". + "2006-1-2 15:4:5.999999999", // 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 +// Copied from yaml.v3. +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/internal/command/select_test.go b/internal/command/select_test.go index 98ef143b..8dbbd3e3 100644 --- a/internal/command/select_test.go +++ b/internal/command/select_test.go @@ -218,4 +218,14 @@ octal: 8`)), nil, )) + t.Run("Issue331 - YAML to JSON", runTest( + []string{"-r", "yaml", "-w", "json"}, + []byte(`createdAt: 2023-06-13T20:19:48.531620935Z`), + newline([]byte(`{ + "createdAt": "2023-06-13T20:19:48.531620935Z" +}`)), + nil, + nil, + )) + }