Skip to content

Commit

Permalink
YAML and JSON.mapping presence: true option (#4843)
Browse files Browse the repository at this point in the history
Allows to distinguish absence of key-value pairs vs null values.
  • Loading branch information
akzhan authored and RX14 committed Aug 18, 2017
1 parent e3763f6 commit 43c52d3
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 0 deletions.
17 changes: 17 additions & 0 deletions spec/std/json/mapping_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ private class JSONWithNilableUnion2
})
end

private class JSONWithPresence
JSON.mapping({
first_name: {type: String?, presence: true, nilable: true},
last_name: {type: String?, presence: true, nilable: true},
})
end

describe "JSON mapping" do
it "parses person" do
person = JSONPerson.from_json(%({"name": "John", "age": 30}))
Expand Down Expand Up @@ -427,4 +434,14 @@ describe "JSON mapping" do
obj.value.should be_nil
obj.to_json.should eq(%({}))
end

describe "parses JSON with presence markers" do
it "parses person with absent attributes" do
json = JSONWithPresence.from_json(%({"first_name": null}))
json.first_name.should be_nil
json.first_name_present?.should be_true
json.last_name.should be_nil
json.last_name_present?.should be_false
end
end
end
17 changes: 17 additions & 0 deletions spec/std/yaml/mapping_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ private class YAMLWithTimeEpochMillis
})
end

private class YAMLWithPresence
YAML.mapping({
first_name: {type: String?, presence: true, nilable: true},
last_name: {type: String?, presence: true, nilable: true},
})
end

describe "YAML mapping" do
it "parses person" do
person = YAMLPerson.from_yaml("---\nname: John\nage: 30\n")
Expand Down Expand Up @@ -288,4 +295,14 @@ describe "YAML mapping" do
yaml.value.should eq(Time.epoch_ms(1459860483856))
yaml.to_yaml.should eq("---\nvalue: 1459860483856\n")
end

describe "parses YAML with presence markers" do
it "parses person with absent attributes" do
yaml = YAMLWithPresence.from_yaml("---\nfirst_name:\n")
yaml.first_name.should be_nil
yaml.first_name_present?.should be_true
yaml.last_name.should be_nil
yaml.last_name_present?.should be_false
end
end
end
15 changes: 15 additions & 0 deletions src/json/mapping.cr
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module JSON
# * **root**: assume the value is inside a JSON object with a given key (see `Object.from_json(string_or_io, root)`)
# * **setter**: if `true`, will generate a setter for the variable, `true` by default
# * **getter**: if `true`, will generate a getter for the variable, `true` by default
# * **presence**: if `true`, a `{{key}}_present?` method will be generated when the key was present (even if it has a `null` value), `false` by default
#
# This macro by default defines getters and setters for each variable (this can be overrided with *setter* and *getter*).
# The mapping doesn't define a constructor accepting these variables as arguments, but you can provide an overload.
Expand Down Expand Up @@ -78,6 +79,14 @@ module JSON
@{{key.id}}
end
{% end %}

{% if value[:presence] %}
@{{key.id}}_present : Bool = false

def {{key.id}}_present?
@{{key.id}}_present
end
{% end %}
{% end %}

def initialize(%pull : ::JSON::PullParser)
Expand Down Expand Up @@ -144,6 +153,12 @@ module JSON
@{{key.id}} = (%var{key.id}).as({{value[:type]}})
{% end %}
{% end %}

{% for key, value in properties %}
{% if value[:presence] %}
@{{key.id}}_present = %found{key.id}
{% end %}
{% end %}
end

def to_json(json : ::JSON::Builder)
Expand Down
16 changes: 16 additions & 0 deletions src/yaml/mapping.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ module YAML
# * *converter* takes an alternate type for parsing. It requires a `#from_yaml` method in that class, and returns an instance of the given type. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`.
# * **setter**: if `true`, will generate a setter for the variable, `true` by default
# * **getter**: if `true`, will generate a getter for the variable, `true` by default
# * **presence**: if `true`, a `{{key}}_present?` method will be generated when the key was present (even if it has a `null` value), `false` by default
#
# This macro by default defines getters and setters for each variable (this can be overrided with *setter* and *getter*).
# The mapping doesn't define a constructor accepting these variables as arguments, but you can provide an overload.
Expand Down Expand Up @@ -85,6 +86,14 @@ module YAML
@{{key.id}}
end
{% end %}

{% if value[:presence] %}
@{{key.id}}_present : Bool = false

def {{key.id}}_present?
@{{key.id}}_present
end
{% end %}
{% end %}

def initialize(%pull : ::YAML::PullParser)
Expand All @@ -100,6 +109,7 @@ module YAML
{% for key, value in properties %}
when {{value[:key] || key.id.stringify}}
%found{key.id} = true

%var{key.id} =
{% if value[:nilable] || value[:default] != nil %} %pull.read_null_or { {% end %}

Expand Down Expand Up @@ -144,6 +154,12 @@ module YAML
@{{key.id}} = %var{key.id}.as({{value[:type]}})
{% end %}
{% end %}

{% for key, value in properties %}
{% if value[:presence] %}
@{{key.id}}_present = %found{key.id}
{% end %}
{% end %}
end

def to_yaml(%yaml : ::YAML::Builder)
Expand Down

0 comments on commit 43c52d3

Please sign in to comment.