From 1a35ccc11031fdfcc446185e0eb180f9216b3e0f Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Thu, 17 Aug 2017 18:44:13 +0300 Subject: [PATCH] YAML | JSON.mapping presence: true option. Allows to distinguish absence of key-value pairs vs null values. --- spec/std/json/mapping_spec.cr | 17 +++++++++++++++++ spec/std/yaml/mapping_spec.cr | 17 +++++++++++++++++ src/json/mapping.cr | 15 +++++++++++++++ src/yaml/mapping.cr | 16 ++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/spec/std/json/mapping_spec.cr b/spec/std/json/mapping_spec.cr index 6e883a351b64..afd1f392e963 100644 --- a/spec/std/json/mapping_spec.cr +++ b/spec/std/json/mapping_spec.cr @@ -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})) @@ -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 diff --git a/spec/std/yaml/mapping_spec.cr b/spec/std/yaml/mapping_spec.cr index 87a629616a76..22b17066127e 100644 --- a/spec/std/yaml/mapping_spec.cr +++ b/spec/std/yaml/mapping_spec.cr @@ -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") @@ -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 diff --git a/src/json/mapping.cr b/src/json/mapping.cr index b93bd7200bfc..d3a67f96fcc0 100644 --- a/src/json/mapping.cr +++ b/src/json/mapping.cr @@ -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. @@ -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) @@ -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) diff --git a/src/yaml/mapping.cr b/src/yaml/mapping.cr index d6ce01749984..61141abf2371 100644 --- a/src/yaml/mapping.cr +++ b/src/yaml/mapping.cr @@ -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. @@ -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) @@ -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 %} @@ -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)