From 53dac75bd3f86bfc6e9881d28863904c1fcddf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Fri, 26 Aug 2016 13:02:47 -0400 Subject: [PATCH 1/8] modelize: precise in `has_value` doc the link with `is_lazy` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- src/modelize/modelize_property.nit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modelize/modelize_property.nit b/src/modelize/modelize_property.nit index 9465dbba41..44d551d02d 100644 --- a/src/modelize/modelize_property.nit +++ b/src/modelize/modelize_property.nit @@ -1158,8 +1158,8 @@ redef class AAttrPropdef # Is the node tagged optional? var is_optional = false - # Has the node a default value? - # Could be through `n_expr` or `n_block` + # Does the node have a default value? + # Could be through `n_expr`, `n_block` or `is_lazy` var has_value = false # The guard associated to a lazy attribute. From 801ddb2afbf5b46197350786869647e49dddbf88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Mon, 29 Aug 2016 22:30:12 -0400 Subject: [PATCH 2/8] frontend/serialization: remove generated documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- src/frontend/serialization_phase.nit | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/frontend/serialization_phase.nit b/src/frontend/serialization_phase.nit index b9cab9d4a4..3a1c9f5815 100644 --- a/src/frontend/serialization_phase.nit +++ b/src/frontend/serialization_phase.nit @@ -315,10 +315,7 @@ do else code.add """ var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}") if not {{{name}}} isa {{{type_name}}} then - # Check if it was a subjectent error v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}") - - # Clear subjacent error if v.keep_going == false then return else self.{{{name}}} = {{{name}}} From 25145a74bd2598f51f89e43ffb4ce0bbf5b862b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Mon, 29 Aug 2016 22:29:30 -0400 Subject: [PATCH 3/8] json::serialization: engines report when an attributes is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/serialization.nit | 4 ++++ lib/serialization/serialization.nit | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index e26b8ec1d9..8a80240996 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -295,6 +295,7 @@ class JsonDeserializer if not root isa Error then errors.add new Error("Deserialization Error: parsed JSON value is not an object.") end + deserialize_attribute_missing = false return null end @@ -302,6 +303,7 @@ class JsonDeserializer if not current.keys.has(name) then errors.add new Error("Deserialization Error: JSON object has not attribute '{name}'.") + deserialize_attribute_missing = true return null end @@ -310,6 +312,8 @@ class JsonDeserializer attributes_path.add name var res = convert_object(value, static_type) attributes_path.pop + + deserialize_attribute_missing = false return res end diff --git a/lib/serialization/serialization.nit b/lib/serialization/serialization.nit index 5630b34acd..db0212258a 100644 --- a/lib/serialization/serialization.nit +++ b/lib/serialization/serialization.nit @@ -100,9 +100,15 @@ abstract class Deserializer # The `static_type` can be used as last resort if the deserialized object # desn't have any metadata declaring the dynamic type. # + # Return the deserialized value or null on error, and set + # `deserialize_attribute_missing` to whether the attribute was missing. + # # Internal method to be implemented by the engines. fun deserialize_attribute(name: String, static_type: nullable String): nullable Object is abstract + # Was the attribute queried by the last call to `deserialize_attribute` missing? + var deserialize_attribute_missing = false + # Register a newly allocated object (even if not completely built) # # Internal method called by objects in creation, to be implemented by the engines. From c7931a8f61f3d74c3f5e8eca0c2ac6c12a6c2320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Tue, 30 Aug 2016 09:37:25 -0400 Subject: [PATCH 4/8] frontend/serialization: leave missing attributes to their default value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- src/frontend/serialization_phase.nit | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/serialization_phase.nit b/src/frontend/serialization_phase.nit index 3a1c9f5815..7fc1c31dae 100644 --- a/src/frontend/serialization_phase.nit +++ b/src/frontend/serialization_phase.nit @@ -314,7 +314,8 @@ do """ else code.add """ var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}") - if not {{{name}}} isa {{{type_name}}} then + if v.deserialize_attribute_missing then + else if not {{{name}}} isa {{{type_name}}} then v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}") if v.keep_going == false then return else From 398b34439a48a2dff84199d9e190c3303fbb6151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Mon, 29 Aug 2016 22:32:44 -0400 Subject: [PATCH 5/8] serialization: don't throw missing attribute error if there is a default value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/serialization.nit | 2 +- src/frontend/serialization_phase.nit | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index 8a80240996..ac29a62e79 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -302,7 +302,7 @@ class JsonDeserializer var current = path.last if not current.keys.has(name) then - errors.add new Error("Deserialization Error: JSON object has not attribute '{name}'.") + # Let the generated code / caller of `deserialize_attribute` raise the missing attribute error deserialize_attribute_missing = true return null end diff --git a/src/frontend/serialization_phase.nit b/src/frontend/serialization_phase.nit index 7fc1c31dae..c0545580a8 100644 --- a/src/frontend/serialization_phase.nit +++ b/src/frontend/serialization_phase.nit @@ -312,9 +312,18 @@ do code.add """ self.{{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}") """ - else code.add """ + else + code.add """ var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}") if v.deserialize_attribute_missing then +""" + # What to do when an attribute is missing? + if attribute.has_value then + # Leave it to the default value + else code.add """ + v.errors.add new Error("Deserialization Error: attribute `{class_name}::{{{name}}}` missing from JSON object")""" + + code.add """ else if not {{{name}}} isa {{{type_name}}} then v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}") if v.keep_going == false then return @@ -322,6 +331,7 @@ do self.{{{name}}} = {{{name}}} end """ + end end code.add "end" From a4d5b668b9586bfd8921b9a3e9dc4ea090dc9f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Tue, 30 Aug 2016 09:14:19 -0400 Subject: [PATCH 6/8] frontend/serialization: missing nullable attribute are set to null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- src/frontend/serialization_phase.nit | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/frontend/serialization_phase.nit b/src/frontend/serialization_phase.nit index c0545580a8..9eb01a4181 100644 --- a/src/frontend/serialization_phase.nit +++ b/src/frontend/serialization_phase.nit @@ -320,6 +320,9 @@ do # What to do when an attribute is missing? if attribute.has_value then # Leave it to the default value + else if mtype isa MNullableType then + code.add """ + self.{{{name}}} = null""" else code.add """ v.errors.add new Error("Deserialization Error: attribute `{class_name}::{{{name}}}` missing from JSON object")""" From 78e07de46bd8a4d8dce1e66e060e1e7e6894c9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Tue, 30 Aug 2016 09:04:05 -0400 Subject: [PATCH 7/8] json::serialization: document reading from JSON with missing attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/serialization.nit | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index ac29a62e79..983f50b7c3 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -134,6 +134,85 @@ # assert deserializer.errors.is_empty # If false, `obj` is invalid # print object # ~~~ +# +# ### Missing attributes and default values +# +# When reading JSON, some attributes expected by Nit classes may be missing. +# The JSON object may come from an external API using optional attributes or +# from a previous version of your program without the attributes. +# When an attribute is not found, the deserialization engine acts in one of three ways: +# +# 1. If the attribute has a default value or if it is annotated by `lazy`, +# the engine leave the attribute to the default value. No error is raised. +# 2. If the static type of the attribute is nullable, the engine sets +# the attribute to `null`. No error is raised. +# 3. Otherwise, the engine raises an error and does not set the attribute. +# The caller must check for `errors` and must not read from the attribute. +# +# ~~~nitish +# import json::serialization +# +# class MyConfig +# serialize +# +# var width: Int # Must be in JSON or an error is raised +# var height = 4 +# var volume_level = 8 is lazy +# var player_name: nullable String +# var tmp_dir: nullable String = "/tmp" is lazy +# end +# +# # --- +# # JSON object with all expected attributes -> OK +# var plain_json = """ +# { +# "width": 11, +# "height": 22, +# "volume_level": 33, +# "player_name": "Alice", +# "tmp_dir": null +# }""" +# var deserializer = new JsonDeserializer(plain_json) +# var obj = new MyConfig.from_deserializer(deserializer) +# +# assert deserializer.errors.is_empty +# assert obj.width == 11 +# assert obj.height == 22 +# assert obj.volume_level == 33 +# assert obj.player_name == "Alice" +# assert obj.tmp_dir == null +# +# # --- +# # JSON object missing optional attributes -> OK +# plain_json = """ +# { +# "width": 11 +# }""" +# deserializer = new JsonDeserializer(plain_json) +# obj = new MyConfig.from_deserializer(deserializer) +# +# assert deserializer.errors.is_empty +# assert obj.width == 11 +# assert obj.height == 4 +# assert obj.volume_level == 8 +# assert obj.player_name == null +# assert obj.tmp_dir == "/tmp" +# +# # --- +# # JSON object missing the mandatory attribute -> Error +# plain_json = """ +# { +# "player_name": "Bob", +# }""" +# deserializer = new JsonDeserializer(plain_json) +# obj = new MyConfig.from_deserializer(deserializer) +# +# # There's an error, `obj` is partial +# assert deserializer.errors.length == 1 +# +# # Still, we can access valid attributes +# assert obj.player_name == "Bob" +# ~~~ module serialization import ::serialization::caching From 9576ee4010a37d73d4fb18b2ddd33ad1bb24d791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Tue, 30 Aug 2016 10:00:44 -0400 Subject: [PATCH 8/8] tests: udate JSON tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- tests/sav/niti/test_json_deserialization_plain_alt2.res | 2 +- tests/sav/test_json_deserialization_plain.res | 1 - tests/sav/test_json_deserialization_plain_alt2.res | 2 +- tests/test_json_deserialization_plain.nit | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/sav/niti/test_json_deserialization_plain_alt2.res b/tests/sav/niti/test_json_deserialization_plain_alt2.res index 102d57b2dc..a6ae6607d3 100644 --- a/tests/sav/niti/test_json_deserialization_plain_alt2.res +++ b/tests/sav/niti/test_json_deserialization_plain_alt2.res @@ -1,3 +1,3 @@ Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:27) # JSON: {"__class": "MyClass", "i": 123, "o": null} -# Errors: 'Deserialization Error: JSON object has not attribute 's'.', 'Deserialization Error: Wrong type on `MyClass::s` expected `String`, got `null`', 'Deserialization Error: JSON object has not attribute 'f'.', 'Deserialization Error: Wrong type on `MyClass::f` expected `Float`, got `null`', 'Deserialization Error: JSON object has not attribute 'a'.', 'Deserialization Error: Wrong type on `MyClass::a` expected `Array[String]`, got `null`' +# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object' diff --git a/tests/sav/test_json_deserialization_plain.res b/tests/sav/test_json_deserialization_plain.res index 71dc9e7a14..62b3c82090 100644 --- a/tests/sav/test_json_deserialization_plain.res +++ b/tests/sav/test_json_deserialization_plain.res @@ -11,7 +11,6 @@ # Nit: > # JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"]} -# Errors: 'Deserialization Error: JSON object has not attribute 'o'.' # Nit: > # JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"], "o": diff --git a/tests/sav/test_json_deserialization_plain_alt2.res b/tests/sav/test_json_deserialization_plain_alt2.res index 54e5a4da8d..4b53f0ffaf 100644 --- a/tests/sav/test_json_deserialization_plain_alt2.res +++ b/tests/sav/test_json_deserialization_plain_alt2.res @@ -1,3 +1,3 @@ Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:22) # JSON: {"__class": "MyClass", "i": 123, "o": null} -# Errors: 'Deserialization Error: JSON object has not attribute 's'.', 'Deserialization Error: Wrong type on `MyClass::s` expected `String`, got `null`', 'Deserialization Error: JSON object has not attribute 'f'.', 'Deserialization Error: Wrong type on `MyClass::f` expected `Float`, got `null`', 'Deserialization Error: JSON object has not attribute 'a'.', 'Deserialization Error: Wrong type on `MyClass::a` expected `Array[String]`, got `null`' +# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object' diff --git a/tests/test_json_deserialization_plain.nit b/tests/test_json_deserialization_plain.nit index e577eda96f..7358c85db9 100644 --- a/tests/test_json_deserialization_plain.nit +++ b/tests/test_json_deserialization_plain.nit @@ -49,7 +49,7 @@ tests.add """ tests.add """ {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "o": null, "a": ["one", "two"], "Some random attribute": 777}""" -# Skipping `o` will cause an error but the attribute will be set to `null` +# Skipping `o` will set the attribute to `null` tests.add """ {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"]}"""