Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ignore_serialize for YAML::Serializable #13556

Merged
merged 9 commits into from
Jun 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions spec/std/yaml/serializable_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,29 @@ class YAMLAttrWithPresence
getter? last_name_present : Bool
end

class YAMLAttrWithPresenceAndIgnoreSerialize
include YAML::Serializable

@[YAML::Field(presence: true, ignore_serialize: ignore_first_name?)]
property first_name : String?

@[YAML::Field(presence: true, ignore_serialize: last_name.nil? && !last_name_present?, emit_null: true)]
property last_name : String?

@[YAML::Field(ignore: true)]
getter? first_name_present : Bool = false

@[YAML::Field(ignore: true)]
getter? last_name_present : Bool = false

def initialize(@first_name : String? = nil, @last_name : String? = nil)
end

def ignore_first_name?
first_name.nil? || first_name == ""
end
end

class YAMLAttrWithQueryAttributes
include YAML::Serializable

Expand Down Expand Up @@ -914,6 +937,53 @@ describe "YAML::Serializable" do
end
end

describe "serializes YAML with presence markers and ignore_serialize" do
context "ignore_serialize is set to a method which returns true when value is nil or empty string" do
it "ignores field when value is empty string" do
yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"first_name": ""}))
yaml.first_name_present?.should be_true
yaml.to_yaml.should eq("--- {}\n")
end

it "ignores field when value is nil" do
yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"first_name": null}))
yaml.first_name_present?.should be_true
yaml.to_yaml.should eq("--- {}\n")
end
end

context "ignore_serialize is set to conditional expressions 'last_name.nil? && !last_name_present?'" do
it "emits null when value is null and @last_name_present is true" do
yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"last_name": null}))
yaml.last_name_present?.should be_true

# libyaml 0.2.5 removes traling space for empty scalar nodes
if YAML.libyaml_version >= SemanticVersion.new(0, 2, 5)
yaml.to_yaml.should eq("---\nlast_name:\n")
else
yaml.to_yaml.should eq("---\nlast_name: \n")
end
end
it "does not emit null when value is null and @last_name_present is false" do
yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({}))
yaml.last_name_present?.should be_false
yaml.to_yaml.should eq("--- {}\n")
end

it "emits field when value is not nil and @last_name_present is false" do
yaml = YAMLAttrWithPresenceAndIgnoreSerialize.new(last_name: "something")
yaml.last_name_present?.should be_false
yaml.to_yaml.should eq("---\nlast_name: something\n")
end

it "emits field when value is not nil and @last_name_present is true" do
yaml = YAMLAttrWithPresenceAndIgnoreSerialize.from_yaml(%({"last_name":"something"}))
yaml.last_name_present?.should be_true
yaml.to_yaml.should eq("---\nlast_name: something\n")
end
end
end

describe "with query attributes" do
it "defines query getter" do
yaml = YAMLAttrWithQueryAttributes.from_yaml(%({"foo": true}))
Expand Down
42 changes: 25 additions & 17 deletions src/yaml/serialization.cr
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ module YAML
#
# `YAML::Field` properties:
# * **ignore**: if `true` skip this field in serialization and deserialization (by default false)
# * **ignore_serialize**: if `true` skip this field in serialization (by default false)
# * **ignore_serialize**: If truthy, skip this field in serialization (default: `false`). The value can be any Crystal expression and is evaluated at runtime.
# * **ignore_deserialize**: if `true` skip this field in deserialization (by default false)
# * **key**: the value of the key in the yaml object (by default the name of the instance variable)
# * **converter**: specify an alternate type for parsing and generation. The converter must define `from_yaml(YAML::ParseContext, YAML::Nodes::Node)` and `to_yaml(value, YAML::Nodes::Builder)`. Examples of converters are a `Time::Format` instance and `Time::EpochConverter` for `Time`.
Expand Down Expand Up @@ -285,12 +285,13 @@ module YAML
{% properties = {} of Nil => Nil %}
{% for ivar in @type.instance_vars %}
{% ann = ivar.annotation(::YAML::Field) %}
{% unless ann && (ann[:ignore] || ann[:ignore_serialize]) %}
{% unless ann && (ann[:ignore] || ann[:ignore_serialize] == true) %}
{%
properties[ivar.id] = {
key: ((ann && ann[:key]) || ivar).id.stringify,
converter: ann && ann[:converter],
emit_null: (ann && (ann[:emit_null] != nil) ? ann[:emit_null] : emit_nulls),
key: ((ann && ann[:key]) || ivar).id.stringify,
converter: ann && ann[:converter],
emit_null: (ann && (ann[:emit_null] != nil) ? ann[:emit_null] : emit_nulls),
ignore_serialize: ann && ann[:ignore_serialize],
}
%}
{% end %}
Expand All @@ -300,23 +301,30 @@ module YAML
{% for name, value in properties %}
_{{name}} = @{{name}}

{% unless value[:emit_null] %}
unless _{{name}}.nil?
{% if value[:ignore_serialize] %}
unless {{value[:ignore_serialize]}}
{% end %}

{{value[:key]}}.to_yaml(yaml)
{% unless value[:emit_null] %}
unless _{{name}}.nil?
{% end %}

{{value[:key]}}.to_yaml(yaml)

{% if value[:converter] %}
if _{{name}}
{{ value[:converter] }}.to_yaml(_{{name}}, yaml)
else
nil.to_yaml(yaml)
{% if value[:converter] %}
if _{{name}}
{{ value[:converter] }}.to_yaml(_{{name}}, yaml)
else
nil.to_yaml(yaml)
end
{% else %}
_{{name}}.to_yaml(yaml)
{% end %}

{% unless value[:emit_null] %}
end
{% else %}
_{{name}}.to_yaml(yaml)
{% end %}

{% unless value[:emit_null] %}
{% if value[:ignore_serialize] %}
end
{% end %}
{% end %}
Expand Down