-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Add support for YAML core schema - Add support for possibly other schemas via the abstract YAML::Parser class - Add missing arguments to YAML::Builder (tag, anchor, scalar) - Add support for parsing and generating recursive data structures - Add support for merge (<<) - Move LibYAML enums to the YAML namespace
- v0.24.1
- 1.15.1
- 1.15.0
- 1.14.1
- 1.14.0
- 1.13.3
- 1.13.2
- 1.13.1
- 1.13.0
- 1.12.2
- 1.12.1
- 1.12.0
- 1.11.2
- 1.11.1
- 1.11.0
- 1.10.1
- 1.10.0
- 1.9.2
- 1.9.1
- 1.9.0
- 1.8.2
- 1.8.1
- 1.8.0
- 1.7.3
- 1.7.2
- 1.7.1
- 1.7.0
- 1.6.2
- 1.6.1
- 1.6.0
- 1.5.1
- 1.5.0
- 1.4.1
- 1.4.0
- 1.3.2
- 1.3.1
- 1.3.0
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.0
- 0.36.1
- 0.36.0
- 0.35.1
- 0.35.0
- 0.34.0
- 0.33.0
- 0.32.1
- 0.32.0
- 0.31.1
- 0.31.0
- 0.30.1
- 0.30.0
- 0.29.0
- 0.28.0
- 0.27.2
- 0.27.1
- 0.27.0
- 0.26.1
- 0.26.0
- 0.25.1
- 0.25.0
- 0.24.2
- 0.24.1
- 0.24.0
Showing
27 changed files
with
2,508 additions
and
489 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
require "spec" | ||
require "yaml" | ||
|
||
private def it_parses(string, expected, file = __FILE__, line = __LINE__) | ||
it "parses #{string.inspect}", file, line do | ||
YAML::Schema::Core.parse(string).should eq(expected) | ||
end | ||
end | ||
|
||
private def it_raises_on_parse(string, message, file = __FILE__, line = __LINE__) | ||
it "raises on parse #{string.inspect}", file, line do | ||
expect_raises(YAML::ParseException, message) do | ||
YAML::Schema::Core.parse(string) | ||
end | ||
end | ||
end | ||
|
||
private def it_parses_scalar(string, expected, file = __FILE__, line = __LINE__) | ||
it "parses #{string.inspect}", file, line do | ||
YAML::Schema::Core.parse_scalar(string).should eq(expected) | ||
end | ||
end | ||
|
||
private def it_parses_string(string, file = __FILE__, line = __LINE__) | ||
it_parses_scalar(string, string, file, line) | ||
end | ||
|
||
private def it_parses_scalar_from_pull(string, expected, file = __FILE__, line = __LINE__) | ||
it_parses_scalar_from_pull(string, file, line) do |value| | ||
value.should eq(expected) | ||
end | ||
end | ||
|
||
private def it_parses_scalar_from_pull(string, file = __FILE__, line = __LINE__, &block : YAML::Type ->) | ||
it "parses #{string.inspect}", file, line do | ||
pull = YAML::PullParser.new(%(value: #{string})) | ||
pull.read_stream_start | ||
pull.read_document_start | ||
pull.read_mapping_start | ||
pull.read_scalar # key | ||
|
||
block.call(YAML::Schema::Core.parse_scalar(pull).as(YAML::Type)) | ||
end | ||
end | ||
|
||
describe YAML::Schema::Core do | ||
# nil | ||
it_parses_scalar "~", nil | ||
it_parses_scalar "null", nil | ||
it_parses_scalar "Null", nil | ||
it_parses_scalar "NULL", nil | ||
|
||
# true | ||
it_parses_scalar "yes", true | ||
it_parses_scalar "Yes", true | ||
it_parses_scalar "YES", true | ||
it_parses_scalar "true", true | ||
it_parses_scalar "True", true | ||
it_parses_scalar "TRUE", true | ||
it_parses_scalar "on", true | ||
it_parses_scalar "On", true | ||
it_parses_scalar "ON", true | ||
|
||
# false | ||
it_parses_scalar "no", false | ||
it_parses_scalar "No", false | ||
it_parses_scalar "NO", false | ||
it_parses_scalar "false", false | ||
it_parses_scalar "False", false | ||
it_parses_scalar "FALSE", false | ||
it_parses_scalar "off", false | ||
it_parses_scalar "Off", false | ||
it_parses_scalar "OFF", false | ||
|
||
# +infinity | ||
it_parses_scalar ".inf", Float64::INFINITY | ||
it_parses_scalar ".Inf", Float64::INFINITY | ||
it_parses_scalar ".INF", Float64::INFINITY | ||
it_parses_scalar "+.inf", Float64::INFINITY | ||
it_parses_scalar "+.Inf", Float64::INFINITY | ||
it_parses_scalar "+.INF", Float64::INFINITY | ||
|
||
# -infinity | ||
it_parses_scalar "-.inf", -Float64::INFINITY | ||
it_parses_scalar "-.Inf", -Float64::INFINITY | ||
it_parses_scalar "-.INF", -Float64::INFINITY | ||
|
||
# nan | ||
it "parses nan" do | ||
{".nan", ".NaN", ".NAN"}.each do |string| | ||
value = YAML::Schema::Core.parse_scalar(string) | ||
value.as(Float64).nan?.should be_true | ||
end | ||
end | ||
|
||
# integer (base 10) | ||
it_parses_scalar "123", 123 | ||
it_parses_scalar "+123", 123 | ||
it_parses_scalar "-123", -123 | ||
|
||
# integer (binary) | ||
it_parses_scalar "0b10110", 0b10110 | ||
|
||
# integer (octal) | ||
it_parses_scalar "0123", 0o123 | ||
|
||
# integer (hex) | ||
it_parses_scalar "0x123abc", 0x123abc | ||
it_parses_scalar "-0x123abc", -0x123abc | ||
|
||
# time | ||
it_parses_scalar "2002-12-14", Time.new(2002, 12, 14, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2", Time.new(2002, 1, 2, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12", Time.new(2002, 1, 2, 10, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2 10:11:12", Time.new(2002, 1, 2, 10, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2 1:11:12", Time.new(2002, 1, 2, 1, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12.3", Time.new(2002, 1, 2, 10, 11, 12, 300, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12.34", Time.new(2002, 1, 2, 10, 11, 12, 340, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12.345", Time.new(2002, 1, 2, 10, 11, 12, 345, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12.3456", Time.new(2002, 1, 2, 10, 11, 12, 345, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12Z", Time.new(2002, 1, 2, 10, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12 Z", Time.new(2002, 1, 2, 10, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12 +3", Time.new(2002, 1, 2, 7, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12 +03:00", Time.new(2002, 1, 2, 7, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12 -03:00", Time.new(2002, 1, 2, 13, 11, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12 -03:31", Time.new(2002, 1, 2, 13, 42, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12-03:31", Time.new(2002, 1, 2, 13, 42, 12, kind: Time::Kind::Utc) | ||
it_parses_scalar "2002-1-2T10:11:12 +0300", Time.new(2002, 1, 2, 7, 11, 12, kind: Time::Kind::Utc) | ||
|
||
# invalid time | ||
it_parses_string "2002-34-45" | ||
it_parses_string "2002-12-14 x" | ||
it_parses_string "2002-1-2T10:11:12x" | ||
it_parses_string "2002-1-2T10:11:12Zx" | ||
it_parses_string "2002-1-2T10:11:12+03x" | ||
|
||
# non-plain style | ||
it_parses_scalar_from_pull %("1"), "1" | ||
|
||
# bools according to the spec, but parsed as strings in Python and Ruby, | ||
# so we do the same in Crystal for "compatibility" | ||
it_parses_scalar "y", "y" | ||
it_parses_scalar "Y", "Y" | ||
it_parses_scalar "n", "n" | ||
it_parses_scalar "N", "N" | ||
|
||
# !!map | ||
it_parses "!!map {1: 2}", {1 => 2} | ||
it_raises_on_parse "!!map 1", "Expected MAPPING_START" | ||
|
||
# !!omap | ||
it_parses "!!omap {1: 2}", {1 => 2} | ||
it_raises_on_parse "!!omap 1", "Expected MAPPING_START" | ||
|
||
# !!pairs | ||
it_parses "!!pairs [{1: 2}, {3: 4}]", [{1 => 2}, {3 => 4}] | ||
it_raises_on_parse "!!pairs 1", "Expected SEQUENCE_START" | ||
it_raises_on_parse "!!pairs [{1: 2, 3: 4}]", "Expected MAPPING_END" | ||
|
||
# !!set | ||
it_parses "!!set { 1, 2, 3 }", Set{1, 2, 3} | ||
it_raises_on_parse "!!set 1", "Expected MAPPING_START" | ||
|
||
# !!seq | ||
it_parses "!!seq [ 1, 2, 3 ]", [1, 2, 3] | ||
it_raises_on_parse "!!seq 1", "Expected SEQUENCE_START" | ||
|
||
# !!binary | ||
it_parses "!!binary aGVsbG8=", "hello".to_slice | ||
it_raises_on_parse "!!binary [1]", "Expected SCALAR" | ||
it_raises_on_parse "!!binary 1", "Error decoding Base64" | ||
|
||
# !!bool | ||
it_parses "!!bool yes", true | ||
it_raises_on_parse "!!bool 1", "Invalid bool" | ||
|
||
# !!float | ||
it_parses "!!float '1.2'", 1.2 | ||
it_parses "!!float '1_234.2'", 1_234.2 | ||
it_parses "!!float .inf", Float64::INFINITY | ||
it_raises_on_parse "!!float 'hello'", "Invalid float" | ||
|
||
# !!int | ||
it_parses "!!int 123", 123 | ||
it_parses "!!int 0b10", 0b10 | ||
it_parses "!!int 0123", 0o123 | ||
it_parses "!!int 0xabc", 0xabc | ||
it_parses "!!int -123", -123 | ||
it_raises_on_parse "!!int 'hello'", "Invalid int" | ||
|
||
# !!null | ||
it_parses "!!null ~", nil | ||
it_raises_on_parse "!!null 1", "Invalid null" | ||
|
||
# !!str | ||
it_parses "!!str 1", "1" | ||
it_raises_on_parse "!!str [1]", "Expected SCALAR" | ||
|
||
# # !!timestamp | ||
it_parses "!!timestamp 2010-01-02", Time.new(2010, 1, 2, kind: Time::Kind::Utc) | ||
it_raises_on_parse "!!timestamp foo", "Invalid timestamp" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,18 @@ | ||
require "yaml" | ||
require "big" | ||
|
||
def BigInt.new(pull : YAML::PullParser) | ||
BigInt.new(pull.read_scalar) | ||
def BigInt.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) | ||
unless node.is_a?(YAML::Nodes::Scalar) | ||
node.raise "Expected scalar, not #{node.class}" | ||
end | ||
|
||
BigInt.new(node.value) | ||
end | ||
|
||
def BigFloat.new(pull : YAML::PullParser) | ||
BigFloat.new(pull.read_scalar) | ||
def BigFloat.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) | ||
unless node.is_a?(YAML::Nodes::Scalar) | ||
node.raise "Expected scalar, not #{node.class}" | ||
end | ||
|
||
BigFloat.new(node.value) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
module YAML | ||
enum EventKind | ||
NONE | ||
STREAM_START | ||
STREAM_END | ||
DOCUMENT_START | ||
DOCUMENT_END | ||
ALIAS | ||
SCALAR | ||
SEQUENCE_START | ||
SEQUENCE_END | ||
MAPPING_START | ||
MAPPING_END | ||
end | ||
|
||
enum ScalarStyle | ||
ANY | ||
PLAIN | ||
SINGLE_QUOTED | ||
DOUBLE_QUOTED | ||
LITERAL | ||
FOLDED | ||
end | ||
|
||
enum SequenceStyle | ||
ANY | ||
BLOCK | ||
FLOW | ||
end | ||
|
||
enum MappingStyle | ||
ANY | ||
BLOCK | ||
FLOW | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
require "./parser" | ||
|
||
# The YAML::Nodes module provides an implementation of an | ||
# in-memory YAML document tree. This tree can be generated | ||
# with the `YAML::Nodes.parse` method or created with a | ||
# `YAML::Nodes::Builder`. | ||
# | ||
# This document tree can then be converted to YAML be | ||
# invoking `to_yaml` on the document object. | ||
module YAML::Nodes | ||
# Parses a `String` or `IO` into a `YAML::Document`. | ||
def self.parse(string_or_io : String | IO) : Document | ||
Parser.new string_or_io, &.parse | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# Builds a tree of YAML nodes. | ||
# | ||
# This builder is similar to `YAML::Builder`, but instead of | ||
# directly emitting the output to an IO it builds a YAML document | ||
# tree in memory. | ||
# | ||
# All "emitting" methods support specifying a "reference" object | ||
# that will be associated to the emitted object, | ||
# so that when that reference object is emitted again an anchor | ||
# and an alias will be created. This generates both more compact | ||
# documents and allows handling recursive data structures. | ||
class YAML::Nodes::Builder | ||
@current : Node | ||
|
||
# The document this builder builds. | ||
getter document : Document | ||
|
||
def initialize | ||
@document = Document.new | ||
@current = @document | ||
@object_id_to_node = {} of UInt64 => Node | ||
@anchor_count = 0 | ||
end | ||
|
||
def scalar(value, anchor : String? = nil, tag : String? = nil, | ||
style : YAML::ScalarStyle = YAML::ScalarStyle::ANY, | ||
reference = nil) : Nil | ||
node = Scalar.new(value.to_s) | ||
node.anchor = anchor | ||
node.tag = tag | ||
node.style = style | ||
|
||
if register(reference, node) | ||
return | ||
end | ||
|
||
push_node(node) | ||
end | ||
|
||
def sequence(anchor : String? = nil, tag : String? = nil, | ||
style : YAML::SequenceStyle = YAML::SequenceStyle::ANY, | ||
reference = nil) : Nil | ||
node = Sequence.new | ||
node.anchor = anchor | ||
node.tag = tag | ||
node.style = style | ||
|
||
if register(reference, node) | ||
return | ||
end | ||
|
||
push_to_stack(node) do | ||
yield | ||
end | ||
end | ||
|
||
def mapping(anchor : String? = nil, tag : String? = nil, | ||
style : YAML::MappingStyle = YAML::MappingStyle::ANY, | ||
reference = nil) : Nil | ||
node = Mapping.new | ||
node.anchor = anchor | ||
node.tag = tag | ||
node.style = style | ||
|
||
if register(reference, node) | ||
return | ||
end | ||
|
||
push_to_stack(node) do | ||
yield | ||
end | ||
end | ||
|
||
private def push_node(node) | ||
case current = @current | ||
when Document | ||
current << node | ||
when Sequence | ||
current << node | ||
when Mapping | ||
current << node | ||
end | ||
end | ||
|
||
private def push_to_stack(node) | ||
push_node(node) | ||
|
||
old_current = @current | ||
@current = node | ||
|
||
yield | ||
|
||
@current = old_current | ||
end | ||
|
||
private def register(object, current_node) | ||
if object.is_a?(Reference) | ||
register_object_id(object.object_id, current_node) | ||
else | ||
false | ||
end | ||
end | ||
|
||
private def register_object_id(object_id, current_node) | ||
node = @object_id_to_node[object_id]? | ||
|
||
if node | ||
anchor = node.anchor ||= begin | ||
@anchor_count += 1 | ||
@anchor_count.to_s | ||
end | ||
|
||
node = Alias.new(anchor) | ||
push_node(node) | ||
true | ||
else | ||
@object_id_to_node[object_id] = current_node | ||
false | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
module YAML::Nodes | ||
# Abstract class of all YAML tree nodes. | ||
abstract class Node | ||
# The optional tag of a node. | ||
property tag : String? | ||
|
||
# The optional anchor of a node. | ||
property anchor : String? | ||
|
||
# The line where this node starts. | ||
property start_line = 0 | ||
|
||
# The column where this node starts. | ||
property start_column = 0 | ||
|
||
# The line where this node ends. | ||
property end_line = 0 | ||
|
||
# The column where this node ends. | ||
property end_column = 0 | ||
|
||
# Returns a tuple of `start_line` and `start_column`. | ||
def location : {Int32, Int32} | ||
{start_line, start_column} | ||
end | ||
|
||
# Raises a `YAML::ParseException` with the given message | ||
# located at this node's `location`. | ||
def raise(message) | ||
::raise YAML::ParseException.new(message, *location) | ||
end | ||
end | ||
|
||
# A YAML document. | ||
class Document < Node | ||
# The nodes inside this document. | ||
# | ||
# A document can hold at most one node. | ||
getter nodes = [] of Node | ||
|
||
# Appends a node to this document. Raises if more | ||
# than one node is appended. | ||
def <<(node) | ||
if nodes.empty? | ||
nodes << node | ||
else | ||
raise ArgumentError.new("Attempted to append more than one node") | ||
end | ||
end | ||
|
||
def to_yaml(builder : YAML::Builder) | ||
nodes.each &.to_yaml(builder) | ||
end | ||
end | ||
|
||
# A scalar value. | ||
class Scalar < Node | ||
# The style of this scalar. | ||
property style : ScalarStyle = ScalarStyle::ANY | ||
|
||
# The value of this scalar. | ||
property value : String | ||
|
||
# Creates a scalar with the given *value*. | ||
def initialize(@value : String) | ||
end | ||
|
||
def to_yaml(builder : YAML::Builder) | ||
builder.scalar(value, anchor, tag, style) | ||
end | ||
end | ||
|
||
# A sequence of nodes. | ||
class Sequence < Node | ||
include Enumerable(Node) | ||
|
||
# The nodes in this sequence. | ||
getter nodes = [] of Node | ||
|
||
# The style of this sequence. | ||
property style : SequenceStyle = SequenceStyle::ANY | ||
|
||
# Appends a node into this sequence. | ||
def <<(node) | ||
@nodes << node | ||
end | ||
|
||
def each | ||
@nodes.each do |node| | ||
yield node | ||
end | ||
end | ||
|
||
def to_yaml(builder : YAML::Builder) | ||
builder.sequence(anchor, tag, style) do | ||
each &.to_yaml(builder) | ||
end | ||
end | ||
end | ||
|
||
# A mapping of nodes. | ||
class Mapping < Node | ||
property style : MappingStyle = MappingStyle::ANY | ||
|
||
# The nodes inside this mapping, stored linearly | ||
# as key1 - value1 - key2 - value2 - etc. | ||
getter nodes = [] of Node | ||
|
||
# Appends two nodes into this mapping. | ||
def []=(key, value) | ||
@nodes << key << value | ||
end | ||
|
||
# Appends a single node into this mapping. | ||
def <<(node) | ||
@nodes << node | ||
end | ||
|
||
# Yields each key-value pair in this mapping. | ||
def each | ||
0.step(by: 2, to: @nodes.size - 1) do |i| | ||
yield({@nodes[i], @nodes[i + 1]}) | ||
end | ||
end | ||
|
||
def to_yaml(builder : YAML::Builder) | ||
builder.mapping(anchor, tag, style) do | ||
each do |key, value| | ||
key.to_yaml(builder) | ||
value.to_yaml(builder) | ||
end | ||
end | ||
end | ||
end | ||
|
||
# An alias. | ||
class Alias < Node | ||
# The node this alias points to. | ||
# This is set by `YAML::Nodes.parse`, and is `nil` by default. | ||
property value : Node? | ||
|
||
# Creates an alias with tha given *anchor*. | ||
def initialize(@anchor : String) | ||
end | ||
|
||
def to_yaml(builder : YAML::Builder) | ||
builder.alias(anchor.not_nil!) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# :nodoc: | ||
class YAML::Nodes::Parser < YAML::Parser | ||
def initialize(content : String | IO) | ||
super | ||
@anchors = {} of String => Node | ||
end | ||
|
||
def self.new(content) | ||
parser = new(content) | ||
yield parser ensure parser.close | ||
end | ||
|
||
def new_documents | ||
[] of Array(Node) | ||
end | ||
|
||
def new_document | ||
Document.new | ||
end | ||
|
||
def new_sequence | ||
sequence = Sequence.new | ||
set_common_properties(sequence) | ||
sequence.style = @pull_parser.sequence_style | ||
sequence | ||
end | ||
|
||
def new_mapping | ||
mapping = Mapping.new | ||
set_common_properties(mapping) | ||
mapping.style = @pull_parser.mapping_style | ||
mapping | ||
end | ||
|
||
def new_scalar | ||
scalar = Scalar.new(@pull_parser.value) | ||
set_common_properties(scalar) | ||
scalar.style = @pull_parser.scalar_style | ||
scalar | ||
end | ||
|
||
private def set_common_properties(node) | ||
node.tag = @pull_parser.tag | ||
node.anchor = @pull_parser.anchor | ||
node.start_line = @pull_parser.start_line.to_i | ||
node.start_column = @pull_parser.start_column.to_i | ||
end | ||
|
||
def end_value(node) | ||
node.end_line = @pull_parser.end_line.to_i | ||
node.end_column = @pull_parser.end_column.to_i | ||
end | ||
|
||
def put_anchor(anchor, value) | ||
@anchors[anchor] = value | ||
end | ||
|
||
def get_anchor(anchor) | ||
value = @anchors.fetch(anchor) do | ||
@pull_parser.raise("Unknown anchor '#{anchor}'") | ||
end | ||
node = Alias.new(anchor) | ||
node.value = value | ||
set_common_properties(node) | ||
node | ||
end | ||
|
||
def process_tag(tag, &block) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Parsing context that holds anchors and what they refer to. | ||
# | ||
# When implementing `new(ctx : YAML::ParseContext, node : YAML::Nodes::Node)` | ||
# to deserialize an object from a node, `Reference` types must invoke | ||
# both `read_alias` and `record_anchor` in order to support parsing | ||
# recursive data structures. | ||
# | ||
# - `read_alias` must be invoked before an instance is created | ||
# - `record_anchor` must be invoked after an instance is created and | ||
# before its members are deserialized. | ||
class YAML::ParseContext | ||
def initialize | ||
# Recorded anchors: anchor => {object_id, crystal_type_id} | ||
@anchors = {} of String => {UInt64, Int32} | ||
end | ||
|
||
# Associates an object with an anchor. | ||
def record_anchor(node, object : T) : Nil forall T | ||
return unless object.is_a?(Reference) | ||
|
||
record_anchor(node.anchor, object.object_id, object.crystal_type_id) | ||
end | ||
|
||
private def record_anchor(anchor, object_id, crystal_type_id) | ||
return unless anchor | ||
|
||
@anchors[anchor] = {object_id, crystal_type_id} | ||
end | ||
|
||
# Tries to read an alias from `node` of type `T`. Invokes | ||
# the block if successful, and invokers must return this object | ||
# instead of deserializing their members. | ||
def read_alias(node, type : T.class) forall T | ||
if ptr = read_alias_impl(node, T.crystal_instance_type_id, raise_on_alias: true) | ||
yield ptr.unsafe_as(T) | ||
end | ||
end | ||
|
||
# Similar to `read_alias` but doesn't raise if an alias exists | ||
# but an instance of type T isn't associated with the current anchor. | ||
def read_alias?(node, type : T.class) forall T | ||
if ptr = read_alias_impl(node, T.crystal_instance_type_id, raise_on_alias: false) | ||
yield ptr.unsafe_as(T) | ||
end | ||
end | ||
|
||
private def read_alias_impl(node, expected_crystal_type_id, raise_on_alias) | ||
if node.is_a?(YAML::Nodes::Alias) | ||
value = @anchors[node.anchor]? | ||
|
||
if value | ||
object_id, crystal_type_id = value | ||
if crystal_type_id == expected_crystal_type_id | ||
return Pointer(Void).new(object_id) | ||
end | ||
end | ||
|
||
raise("Error deserailizing alias") if raise_on_alias | ||
end | ||
|
||
nil | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
# Provides utility methods for the YAML 1.1 core schema | ||
# with the additional independent types specified in http://yaml.org/type/ | ||
module YAML::Schema::Core | ||
# Deserializes a YAML document. | ||
# | ||
# Same as `YAML.parse`. | ||
def self.parse(data : String | IO) | ||
Parser.new data, &.parse | ||
end | ||
|
||
# Deserializes multiple YAML documents. | ||
# | ||
# Same as `YAML.parse_all`. | ||
def self.parse_all(data : String | IO) | ||
Parser.new data, &.parse_all | ||
end | ||
|
||
# Assuming the *pull_parser* is positioned in a scalar, | ||
# parses it according to the core schema, taking the | ||
# scalar's style and tag into account, then advances | ||
# the pull parser. | ||
def self.parse_scalar(pull_parser : YAML::PullParser) : Type | ||
string = pull_parser.value | ||
|
||
# Check for core schema tags | ||
process_scalar_tag(pull_parser, pull_parser.tag) do |value| | ||
return value | ||
end | ||
|
||
# Non-plain scalar is always a string | ||
unless pull_parser.scalar_style.plain? | ||
return string | ||
end | ||
|
||
parse_scalar(string) | ||
end | ||
|
||
# Parses a scalar value from the given *node*. | ||
def self.parse_scalar(node : YAML::Nodes::Scalar) : Type | ||
string = node.value | ||
|
||
# Check for core schema tags | ||
process_scalar_tag(node) do |value| | ||
return value | ||
end | ||
|
||
# Non-plain scalar is always a string | ||
unless node.style.plain? | ||
return string | ||
end | ||
|
||
parse_scalar(string) | ||
end | ||
|
||
# Parses a string according to the core schema, assuming | ||
# the string had a plain style. | ||
# | ||
# ``` | ||
# YAML::Schema::Core.parse_scalar("hello") # => "hello" | ||
# YAML::Schema::Core.parse_scalar("1.2") # => 1.2 | ||
# YAML::Schema::Core.parse_scalar("false") # => false | ||
# ``` | ||
def self.parse_scalar(string : String) : Type | ||
if parse_null?(string) | ||
return nil | ||
end | ||
|
||
value = parse_bool?(string) | ||
return value unless value.nil? | ||
|
||
value = parse_float_infinity_and_nan?(string) | ||
return value if value | ||
|
||
# Optimizations for prefixes that either parse to | ||
# a number or are strings otherwise | ||
case string | ||
when .starts_with?("0x"), | ||
.starts_with?("+0x"), | ||
.starts_with?("-0x") | ||
value = string.to_i64?(base: 16, prefix: true) | ||
return value || string | ||
when .starts_with?('0') | ||
value = string.to_i64?(base: 8, prefix: true) | ||
return value || string | ||
when .starts_with?('-'), | ||
.starts_with?('+') | ||
value = parse_number?(string) | ||
return value || string | ||
end | ||
|
||
if string[0].ascii_number? | ||
value = parse_number?(string) | ||
return value if value | ||
|
||
value = parse_time?(string) | ||
return value if value | ||
end | ||
|
||
string | ||
end | ||
|
||
# Returns whether a string is reserved and must non be output | ||
# with a plain style, according to the core schema. | ||
# | ||
# ``` | ||
# YAML::Schema::Core.reserved_string?("hello") # => false | ||
# YAML::Schema::Core.reserved_string?("1.2") # => true | ||
# YAML::Schema::Core.reserved_string?("false") # => true | ||
# ``` | ||
def self.reserved_string?(string) : Bool | ||
# There's simply no other way than parsing the string and | ||
# checking what we got. | ||
# | ||
# The performance loss is minimal because `parse_scalar` | ||
# doesn't allocate memory: it can only return primitive | ||
# types, or `Time`, which is a struct. | ||
!parse_scalar(string).is_a?(String) | ||
end | ||
|
||
# If `node` parses to a null value, returns `nil`, otherwise | ||
# invokes the given block. | ||
def self.parse_null_or(node : YAML::Nodes::Node) | ||
if node.is_a?(YAML::Nodes::Scalar) && parse_null?(node.value) | ||
nil | ||
else | ||
yield | ||
end | ||
end | ||
|
||
# Invokes the block for each of the given *node*s keys and | ||
# values, resolving merge keys (<<) when found (keys and | ||
# values of the resolved merge mappings are yielded, | ||
# recursively). | ||
def self.each(node : YAML::Nodes::Mapping) | ||
# We can't just traverse the nodes and invoke yield because | ||
# yield can't recurse. So, we use a stack of {Mapping, index}. | ||
# We pop from the stack and traverse the mapping values. | ||
# When we find a merge, we stop (put back in the stack with | ||
# that mapping and next index) and add solved mappings from | ||
# the merge to the stack, and continue processing. | ||
|
||
stack = [{node, 0}] | ||
|
||
# Mappings that we already visited. In case of a recursion | ||
# we want to stop. For example: | ||
# | ||
# foo: &foo | ||
# <<: *foo | ||
# | ||
# When we traverse &foo we'll put it in visited, | ||
# and when we find it in *foo we'll skip it. | ||
# | ||
# This has no use case, but we don't want to hang the program. | ||
visited = Set(YAML::Nodes::Mapping).new | ||
|
||
until stack.empty? | ||
mapping, index = stack.pop | ||
|
||
visited << mapping | ||
|
||
while index < mapping.nodes.size | ||
key = mapping.nodes[index] | ||
index += 1 | ||
|
||
value = mapping.nodes[index] | ||
index += 1 | ||
|
||
if key.is_a?(YAML::Nodes::Scalar) && | ||
key.value == "<<" && | ||
key.tag != "tag:yaml.org,2002:str" && | ||
solve_merge(stack, mapping, index, value, visited) | ||
break | ||
else | ||
yield({key, value}) | ||
end | ||
end | ||
end | ||
end | ||
|
||
private def self.solve_merge(stack, mapping, index, value, visited) | ||
value = value.value if value.is_a?(YAML::Nodes::Alias) | ||
|
||
case value | ||
when YAML::Nodes::Mapping | ||
stack.push({mapping, index}) | ||
|
||
unless visited.includes?(value) | ||
stack.push({value, 0}) | ||
end | ||
|
||
true | ||
when YAML::Nodes::Sequence | ||
all_mappings = value.nodes.all? do |elem| | ||
elem = elem.value if elem.is_a?(YAML::Nodes::Alias) | ||
elem.is_a?(YAML::Nodes::Mapping) | ||
end | ||
|
||
if all_mappings | ||
stack.push({mapping, index}) | ||
|
||
value.each do |elem| | ||
elem = elem.value if elem.is_a?(YAML::Nodes::Alias) | ||
mapping = elem.as(YAML::Nodes::Mapping) | ||
|
||
unless visited.includes?(mapping) | ||
stack.push({mapping, 0}) | ||
end | ||
end | ||
|
||
true | ||
else | ||
false | ||
end | ||
else | ||
false | ||
end | ||
end | ||
|
||
protected def self.parse_binary(string, location) : Bytes | ||
Base64.decode(string) | ||
rescue ex : Base64::Error | ||
raise YAML::ParseException.new("Error decoding Base64: #{ex.message}", *location) | ||
end | ||
|
||
protected def self.parse_bool(string, location) : Bool | ||
value = parse_bool?(string) | ||
unless value.nil? | ||
return value | ||
end | ||
|
||
raise YAML::ParseException.new("Invalid bool", *location) | ||
end | ||
|
||
protected def self.parse_int(string, location) : Int64 | ||
string.to_i64?(underscore: true, prefix: true) || | ||
raise(YAML::ParseException.new("Invalid int", *location)) | ||
end | ||
|
||
protected def self.parse_float(string, location) : Float64 | ||
parse_float_infinity_and_nan?(string) || | ||
parse_float?(string) || | ||
raise(YAML::ParseException.new("Invalid float", *location)) | ||
end | ||
|
||
protected def self.parse_null(string, location) : Nil | ||
if parse_null?(string) | ||
return nil | ||
end | ||
|
||
raise YAML::ParseException.new("Invalid null", *location) | ||
end | ||
|
||
protected def self.parse_time(string, location) : Time | ||
parse_time?(string) || | ||
raise(YAML::ParseException.new("Invalid timestamp", *location)) | ||
end | ||
|
||
protected def self.process_scalar_tag(scalar) | ||
process_scalar_tag(scalar, scalar.tag) do |value| | ||
yield value | ||
end | ||
end | ||
|
||
protected def self.process_scalar_tag(source, tag) | ||
case tag | ||
when "tag:yaml.org,2002:binary" | ||
yield parse_binary(source.value, source.location) | ||
when "tag:yaml.org,2002:bool" | ||
yield parse_bool(source.value, source.location) | ||
when "tag:yaml.org,2002:float" | ||
yield parse_float(source.value, source.location) | ||
when "tag:yaml.org,2002:int" | ||
yield parse_int(source.value, source.location) | ||
when "tag:yaml.org,2002:null" | ||
yield parse_null(source.value, source.location) | ||
when "tag:yaml.org,2002:str" | ||
yield source.value | ||
when "tag:yaml.org,2002:timestamp" | ||
yield parse_time(source.value, source.location) | ||
end | ||
end | ||
|
||
private def self.parse_null?(string) | ||
case string | ||
when .empty?, "~", "null", "Null", "NULL" | ||
true | ||
else | ||
false | ||
end | ||
end | ||
|
||
private def self.parse_bool?(string) | ||
case string | ||
when "yes", "Yes", "YES", "true", "True", "TRUE", "on", "On", "ON" | ||
true | ||
when "no", "No", "NO", "false", "False", "FALSE", "off", "Off", "OFF" | ||
false | ||
else | ||
nil | ||
end | ||
end | ||
|
||
private def self.parse_number?(string) | ||
parse_int?(string) || parse_float?(string) | ||
end | ||
|
||
private def self.parse_int?(string) | ||
string.to_i64?(underscore: true) | ||
end | ||
|
||
private def self.parse_float?(string) | ||
string = string.delete('_') if string.includes?('_') | ||
string.to_f64? | ||
end | ||
|
||
private def self.parse_float_infinity_and_nan?(string) | ||
case string | ||
when ".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF" | ||
Float64::INFINITY | ||
when "-.inf", "-.Inf", "-.INF" | ||
-Float64::INFINITY | ||
when ".nan", ".NaN", ".NAN" | ||
Float64::NAN | ||
else | ||
nil | ||
end | ||
end | ||
|
||
private def self.parse_time?(string) | ||
# Minimum length is that of YYYY-M-D | ||
return nil if string.size < 8 | ||
|
||
TimeParser.new(string).parse | ||
end | ||
end |
Oops, something went wrong.