Skip to content
16 changes: 16 additions & 0 deletions spec/std/yaml/mapping_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ private class StrictYAMLPerson
}, true)
end

private class YAMLPersonWithExtras
YAML.mapping({
name: {type: String},
age: {type: Int32, nilable: true},
}, extra: "extras")
end

private class YAMLWithBool
YAML.mapping value: Bool
end
Expand Down Expand Up @@ -132,6 +139,15 @@ describe "YAML mapping" do
StrictYAMLPerson.from_yaml("---\nname: John\nfoo: [1, 2, 3]\nage: 30\n")
end
end
it "parses person with extra attributes" do
person = YAMLPersonWithExtras.from_yaml("---\nname: John\nunknown: [1, 2, 3]\nage: 30\n")
person.should be_a(YAMLPersonWithExtras)
person.name.should eq("John")
person.age.should eq(30)
person.extras.should be_a(Hash(String, YAML::Any))
person.extras["unknown"].should be_a(YAML::Any)
person.extras["unknown"].size.should eq(3)
end

it "does to_yaml" do
person = YAMLPerson.from_yaml("---\nname: John\nage: 30\n")
Expand Down
28 changes: 27 additions & 1 deletion src/yaml/mapping.cr
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ module YAML
# it and initializes this type's instance variables.
#
# This macro also declares instance variables of the types given in the mapping.
macro mapping(properties, strict = false)
macro mapping(properties, strict = false, extra = nil)
{% if extra && properties.keys.includes? extra.id %}
{{ raise "Name for extra property already in use: #{extra.id}" }}
{% end %}

{% for key, value in properties %}
{% properties[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %}
{% end %}
Expand All @@ -87,12 +91,24 @@ module YAML
{% end %}
{% end %}

{% if extra %}
@{{extra.id}} : Hash(String, ::YAML::Any)

def {{extra.id}}
@{{extra.id}}
end
{% end %}

def initialize(%pull : ::YAML::PullParser)
{% for key, value in properties %}
%var{key.id} = nil
%found{key.id} = false
{% end %}

{% if extra %}
@{{extra.id}} = Hash(String, ::YAML::Any).new
{% end %}

%pull.read_mapping_start
while %pull.kind != ::YAML::EventKind::MAPPING_END
key = %pull.read_scalar.not_nil!
Expand All @@ -116,6 +132,8 @@ module YAML
else
{% if strict %}
raise ::YAML::ParseException.new("Unknown yaml attribute: #{key}", 0, 0)
{% elsif extra %}
@{{extra.id}}[key] = ::YAML::Any.new(%pull)
{% else %}
%pull.skip
{% end %}
Expand Down Expand Up @@ -163,6 +181,14 @@ module YAML
{% end %}
end
{% end %}
{% if extra %}
_{{extra.id}} = @{{extra.id}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using % fresh variables.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but why not just call .to_yaml on the @ instance variable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if @{{extra.id}} exists, then it is not nillable so you could call @{{extra.id}}.to_yaml(... directly. For memory footprint I would suggest to let it be lazy initialized / nilable. But for the use case, if the user expect to have some extras, maybe is good enough to eagerly create a and instance for @{{extra.id}}.

@aaronbronow aaronbronow May 15, 2017

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this implementation extra is nil unless the user has set it and passed it in as an argument to the mapping(...) macro. Because the macro checks for existence of extra before emitting code, @{{extra.id}} won't exist at all if the user is not using extras. If extra does exist then @{{extra.id}} will always get initialized as a Hash(String, ::YAML::Any)

If I'm understanding this correctly, the only problem with trying to call @{{extra.id}}.to_yaml(... would be if @{{extra.id}} is nil at the time of executing this block and that's impossible. I hope I'm understanding correctly 😄


unless _{{extra.id}}.nil?
"{{extra.id}}".to_yaml(%yaml)
_{{extra.id}}.to_yaml(%yaml)
end
{% end %}
end
end
end
Expand Down