# An invalid attribute name is an heuristic for invalid data.
# Hitting an object end marker will result in an empty string.
- assert next_attribute_name.is_valid_id else
+ if not next_attribute_name.is_valid_id then
var error
if next_attribute_name.is_empty then
do
for i in substrings do s.write_native(i.to_cstring, 0, i.byte_length)
end
-end
-redef class String
# return true if a file with this names exists
fun file_exists: Bool do return to_cstring.file_exists
+end
+redef class String
# The status of a file. see POSIX stat(2).
fun file_stat: nullable FileStat
do
path.add('/')
end
var error: nullable Error = null
- for d in dirs do
+ for i in [0 .. dirs.length - 1[ do
+ var d = dirs[i]
if d.is_empty then continue
path.append(d)
path.add('/')
- var res = path.to_s.to_cstring.file_mkdir(mode)
+ if path.file_exists then continue
+ var res = path.to_cstring.file_mkdir(mode)
if not res and error == null then
error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
end
end
+ var res = self.to_cstring.file_mkdir(mode)
+ if not res and error == null then
+ error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
+ end
return error
end
# 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
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
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
attributes_path.add name
var res = convert_object(value, static_type)
attributes_path.pop
+
+ deserialize_attribute_missing = false
return res
end
# 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.
var errors = new Array[Error]
end
-# Deserialization got wrong attribute names
+# Error on invalid dynamic type for a deserialized attribute
class AttributeTypeError
super Error
-<ul class='nav nav-tabs' role='tablist'>
+<ul class='nav nav-tabs' ng-init='entityCtrl.loadEntityLinearization()'>
<li role='presentation' class='warning'>
<a ng-href='{{mentity.mclass.web_url}}'>
<span class='glyphicon glyphicon-chevron-left'/> Go to class
-<ul class='nav nav-tabs'>
+<ul class='nav nav-tabs' ng-init='entityCtrl.loadEntityLinearization()'>
<li role='presentation' class='warning'>
<a href='{{mentity.mproperty.web_url}}'>
<span class='glyphicon glyphicon-chevron-left'/> Go to property
if type_name == "nullable Object" then
# Don't type check
code.add """
- var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
+ 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 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}}}")
+ 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 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")"""
- # Clear subjacent error
+ 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
else
self.{{{name}}} = {{{name}}}
end
"""
+ end
end
code.add "end"
# Return `self` as a JsonObject.
#
- # By default, every reference to another MEntity is replaced by a pointer
- # to the MEntity::json_id.
+ # By default, every reference to another MEntity is skipped.
+ # Use full_json for a full representation.
fun json: JsonObject do
var obj = new JsonObject
obj["name"] = name
obj["full_name"] = full_name
obj["mdoc"] = mdoc_or_fallback
obj["visibility"] = visibility
- obj["location"] = location
var modifiers = new JsonArray
for modifier in collect_modifiers do
modifiers.add modifier
end
redef fun to_json do return json.to_json
+
+ # Return `self` as a JsonObject with references.
+ #
+ # By default, every reference to another MEntity is replaced by a pointer
+ # to the MEntity::json_id.
+ fun full_json: JsonObject do
+ var obj = json
+ obj["location"] = location
+ return obj
+ end
+
+ # Return `full_json` as a json string.
+ fun to_full_json: String do return full_json.to_json
end
redef class MDoc
redef class MPackage
- redef fun json do
+ redef fun full_json do
var obj = super
if ini != null then
obj["ini"] = new JsonObject.from(ini.as(not null).to_map)
redef fun json do
var obj = super
obj["is_root"] = is_root
+ return obj
+ end
+
+ redef fun full_json do
+ var obj = super
obj["mpackage"] = to_mentity_ref(mpackage)
obj["default_mmodule"] = to_mentity_ref(default_mmodule)
obj["parent"] = to_mentity_ref(parent)
end
redef class MModule
- redef fun json do
+ redef fun full_json do
var obj = super
obj["mpackage"] = to_mentity_ref(mpackage)
obj["mgroup"] = to_mentity_ref(mgroup)
obj["intro_mclasses"] = to_mentity_refs(intro_mclasses)
obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+ obj["intro_mclassdefs"] = to_mentity_refs(collect_intro_mclassdefs(private_view))
+ obj["redef_mclassdefs"] = to_mentity_refs(collect_redef_mclassdefs(private_view))
+ obj["imports"] = to_mentity_refs(in_importation.direct_greaters)
return obj
end
end
redef class MClass
redef fun json do
var obj = super
- var arr = new JsonArray
- for mparameter in mparameters do arr.add mparameter
- obj["mparameters"] = arr
+ obj["mparameters"] = new JsonArray.from(mparameters)
+ return obj
+ end
+
+ redef fun full_json do
+ var obj = super
obj["intro"] = to_mentity_ref(intro)
obj["intro_mmodule"] = to_mentity_ref(intro_mmodule)
obj["mpackage"] = to_mentity_ref(intro_mmodule.mpackage)
obj["mclassdefs"] = to_mentity_refs(mclassdefs)
+ obj["all_mproperties"] = to_mentity_refs(collect_accessible_mproperties(private_view))
+ obj["intro_mproperties"] = to_mentity_refs(collect_intro_mproperties(private_view))
+ obj["redef_mproperties"] = to_mentity_refs(collect_redef_mproperties(private_view))
+ obj["parents"] = to_mentity_refs(collect_parents(private_view))
return obj
end
end
redef fun json do
var obj = super
obj["is_intro"] = is_intro
- var arr = new JsonArray
- for mparameter in mclass.mparameters do arr.add mparameter
- obj["mparameters"] = arr
+ obj["mparameters"] = new JsonArray.from(mclass.mparameters)
+ return obj
+ end
+
+ redef fun full_json do
+ var obj = super
obj["mmodule"] = to_mentity_ref(mmodule)
obj["mclass"] = to_mentity_ref(mclass)
obj["mpropdefs"] = to_mentity_refs(mpropdefs)
obj["intro_mproperties"] = to_mentity_refs(intro_mproperties)
+ obj["intro"] = to_mentity_ref(mclass.intro)
+ obj["mpackage"] = to_mentity_ref(mmodule.mpackage)
+ obj["intro_mpropdefs"] = to_mentity_refs(collect_intro_mpropdefs(private_view))
+ obj["redef_mpropdefs"] = to_mentity_refs(collect_redef_mpropdefs(private_view))
return obj
end
end
redef class MProperty
- redef fun json do
+ redef fun full_json do
var obj = super
obj["intro"] = to_mentity_ref(intro)
obj["intro_mclassdef"] = to_mentity_ref(intro_mclassdef)
obj["mpropdefs"] = to_mentity_refs(mpropdefs)
+ obj["intro_mclass"] = to_mentity_ref(intro_mclassdef.mclass)
+ obj["mpackage"] = to_mentity_ref(intro_mclassdef.mmodule.mpackage)
return obj
end
end
redef fun json do
var obj = super
obj["is_intro"] = is_intro
+ return obj
+ end
+
+ redef fun full_json do
+ var obj = super
obj["mclassdef"] = to_mentity_ref(mclassdef)
obj["mproperty"] = to_mentity_ref(mproperty)
+ obj["intro"] = to_mentity_ref(mproperty.intro)
+ obj["intro_mclassdef"] = to_mentity_ref(mproperty.intro.mclassdef)
+ obj["mmodule"] = to_mentity_ref(mclassdef.mmodule)
+ obj["mgroup"] = to_mentity_ref(mclassdef.mmodule.mgroup)
+ obj["mpackage"] = to_mentity_ref(mclassdef.mmodule.mpackage)
return obj
end
end
# 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.
res.api_error(404, "No linearization for mentity `{mentity.full_name}`")
return
end
- res.json new JsonArray.from(lin)
+ var mentities = new JsonArray
+ for e in lin do mentities.add e.full_json
+ res.json mentities
end
end
end
# Get the full json repesentation of `self` with MEntityRefs resolved.
- fun api_json(handler: APIHandler): JsonObject do return json
+ fun api_json(handler: APIHandler): JsonObject do return full_json
end
redef class MEntityRef
obj["name"] = mentity.name
obj["mdoc"] = mentity.mdoc_or_fallback
obj["visibility"] = mentity.visibility
- obj["location"] = mentity.location
var modifiers = new JsonArray
for modifier in mentity.collect_modifiers do
modifiers.add modifier
end
return obj
end
-end
-
-redef class MDoc
-
- # Add doc down processing
- redef fun json do
- var obj = super
- obj["synopsis"] = synopsis
- obj["documentation"] = documentation
- obj["comment"] = comment
- obj["html_synopsis"] = html_synopsis.write_to_string
- obj["html_documentation"] = html_documentation.write_to_string
- obj["html_comment"] = html_comment.write_to_string
- return obj
- end
-end
-
-redef class MModule
- redef fun api_json(handler) do
- var obj = super
- obj["intro_mclassdefs"] = to_mentity_refs(collect_intro_mclassdefs(private_view))
- obj["redef_mclassdefs"] = to_mentity_refs(collect_redef_mclassdefs(private_view))
- obj["imports"] = to_mentity_refs(in_importation.direct_greaters)
- return obj
- end
-end
-
-redef class MClass
- redef fun api_json(handler) do
- var obj = super
- obj["all_mproperties"] = to_mentity_refs(collect_accessible_mproperties(private_view))
- obj["intro_mproperties"] = to_mentity_refs(collect_intro_mproperties(private_view))
- obj["redef_mproperties"] = to_mentity_refs(collect_redef_mproperties(private_view))
- obj["parents"] = to_mentity_refs(collect_parents(private_view))
- return obj
- end
-end
-
-redef class MClassDef
- redef fun json do
- var obj = super
- obj["intro"] = to_mentity_ref(mclass.intro)
- obj["mpackage"] = to_mentity_ref(mmodule.mpackage)
- return obj
- end
- redef fun api_json(handler) do
+ redef fun full_json do
var obj = super
- obj["intro_mpropdefs"] = to_mentity_refs(collect_intro_mpropdefs(private_view))
- obj["redef_mpropdefs"] = to_mentity_refs(collect_redef_mpropdefs(private_view))
+ obj["location"] = mentity.location
return obj
end
end
-redef class MProperty
- redef fun json do
- var obj = super
- obj["intro_mclass"] = to_mentity_ref(intro_mclassdef.mclass)
- obj["mpackage"] = to_mentity_ref(intro_mclassdef.mmodule.mpackage)
- return obj
- end
-end
+redef class MDoc
-redef class MPropDef
+ # Add doc down processing
redef fun json do
- var obj = super
- obj["intro"] = to_mentity_ref(mproperty.intro)
- obj["intro_mclassdef"] = to_mentity_ref(mproperty.intro.mclassdef)
- obj["mmodule"] = to_mentity_ref(mclassdef.mmodule)
- obj["mgroup"] = to_mentity_ref(mclassdef.mmodule.mgroup)
- obj["mpackage"] = to_mentity_ref(mclassdef.mmodule.mpackage)
+ var obj = new JsonObject
+ obj["html_synopsis"] = html_synopsis.write_to_string
+ obj["html_documentation"] = html_documentation.write_to_string
return obj
end
end
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'
# Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>
# JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"]}
-# Errors: 'Deserialization Error: JSON object has not attribute 'o'.'
# Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<null>>
# JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"], "o":
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'
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"]}"""