# See the License for the specific language governing permissions and
# limitations under the License.
-# Dynamic interface to read Json strings.
+# Dynamic interface to read JSON strings.
#
# `String::to_json_value` returns a `JsonValue` which can be queried
-# to get the underlying Json data. It can also be used as any Json types.
+# to get the underlying JSON data.
module dynamic
+import error
private import static
-import standard
+# Wraps a JSON value.
+#
+# Offer methods to query the type, to dynamicaly cast the underlying value and
+# to query elements (in case of a JSON object or a JSON array).
+#
+# Use `String::to_json_value` to get a `JsonValue` from a string.
class JsonValue
+
+ # The wrapped JSON value.
var value: nullable Object
# Is this value null?
# Get this value as a `Int`
#
- # require: `self.is_int`
+ # require: `self.is_numeric`
#
# assert "-10".to_json_value.to_i == -10
# assert "123".to_json_value.to_i == 123
- fun to_i: Int do return value.as(Int)
+ # assert "123.456".to_json_value.to_i == 123
+ fun to_i: Int
+ do
+ var value = value
+ assert value isa Numeric
+ return value.to_i
+ end
# Is this value a float?
#
# Get this value as a `Float`
#
- # require: `self.is_float`
+ # require: `self.is_numeric`
#
# assert "0.0".to_json_value.to_f == 0.0
# assert "123.456".to_json_value.to_f == 123.456
- fun to_f: Float do return value.as(Float)
+ # assert "123".to_json_value.to_f == 123.0
+ fun to_f: Float
+ do
+ var value = value
+ assert value isa Numeric
+ return value.to_f
+ end
# Is the value numeric?
#
#
# require: `self.is_numeric`
#
- # assert "1.234".to_json_value.to_numeric = 1.234
- # assert "1234".to_json_value.to_numeric = 1234
+ # assert "1.234".to_json_value.to_numeric == 1.234
+ # assert "1234".to_json_value.to_numeric == 1234
fun to_numeric: Numeric
do
if is_int then return to_i
# assert "\"str\"".to_json_value.to_s == "str"
# assert "123".to_json_value.to_s == "123"
# assert "true".to_json_value.to_s == "true"
- # assert "[1, 2, 3]".to_json_value.to_s == "123"
- redef fun to_s: String
- do
+ # assert "[1, 2, 3]".to_json_value.to_s == "[1,2,3]"
+ redef fun to_s do
if value == null then return "null"
return value.to_s
end
return a
end
+ ### Error
+
+ # Is this value an error?
+ #
+ # assert "[]".to_json_value[0].is_error
+ # assert "[".to_json_value.is_error
+ # assert not "[]".to_json_value.is_error
+ fun is_error: Bool do return value isa Error
+
+ # Get this value as a `Error`.
+ #
+ # require: `self.is_error`
+ fun to_error: Error do return value.as(Error)
+
+ ### JsonParseError
+
+ # Is this value a parse error?
+ #
+ # assert "[".to_json_value.is_parse_error
+ # assert not "[]".to_json_value.is_parse_error
+ fun is_parse_error: Bool do return value isa JsonParseError
+
+ # Get this value as a `JsonParseError`.
+ #
+ # require: `self.is_parse_error`
+ fun to_parse_error: JsonParseError do return value.as(JsonParseError)
+
+ ### Children access
+
# Iterator over the values of the array `self`
#
# require: `self.is_array`
#
# assert """{"a": 123}""".to_json_value["a"].to_i == 123
# assert """{"123": "a"}""".to_json_value[123].to_s == "a"
- # assert """{"John Smith": 1980}""".to_json_value[["John ", "Smith"]].to_i == 1980
+ # assert """{"John Smith": 1980}""".to_json_value["John Smith"].to_i == 1980
+ # assert """{"a": 123}""".to_json_value["b"].is_error
#
# assert """["a", "b", "c"]""".to_json_value[0].to_s == "a"
- fun [](key: Object): JsonValue
- do
+ # assert """["a", "b", "c"]""".to_json_value[3].is_error
+ fun [](key: Object): JsonValue do
var value = value
- if value isa MapRead[String, nullable Object] then
- return new JsonValue(value[key.to_s])
+ var result: nullable Object
+ if is_error then
+ return self
+ else if value isa MapRead[String, nullable Object] then
+ key = key.to_s
+ if value.has_key(key) then
+ result = value[key]
+ else
+ result = new JsonKeyError("Key `{key}` not found.", self, key)
+ end
else if value isa SequenceRead[nullable Object] then
- assert key isa Int
- return new JsonValue(value[key])
- else abort
+ if key isa Int then
+ if key < value.length and key >= 0 then
+ result = value[key]
+ else
+ result = new JsonKeyError("Index `{key}` out of bounds.",
+ self, key)
+ end
+ else
+ result = new JsonKeyError("Invalid key type. Expecting `Int`. Got `{key.class_name}`.",
+ self, key)
+ end
+ else
+ result = new JsonKeyError("Invalid `[]` access on a `{json_type}` JsonValue.",
+ self, key)
+ end
+ return new JsonValue(result)
end
# Advanced query to get a value within the map `self` or it's children.
# assert """{"a": {"t": true, "f": false}}""".to_json_value.get("a").is_map
# assert """{"a": {"t": true, "f": false}}""".to_json_value.get("a.t").to_bool
# assert not """{"a": {"t": true, "f": false}}""".to_json_value.get("a.f").to_bool
+ # assert """{"a": {"t": true, "f": false}}""".to_json_value.get("a.t.t").is_error
# assert """{"a": {"b": {"c": {"d": 123}}}}""".to_json_value.get("a.b.c.d").to_i == 123
- fun get(query: String): JsonValue
- do
+ # assert """{"a": {"b": {"c": {"d": 123}}}}""".to_json_value.get("a.z.c.d").is_error
+ fun get(query: String): JsonValue do
var keys = query.split(".")
var value = value
- for key in keys do
- assert value isa MapRead[String, nullable Object]
- value = value[key]
+ if is_error then return self
+ for i in [0..keys.length[ do
+ var key = keys[i]
+ if value isa MapRead[String, nullable Object] then
+ if value.has_key(key) then
+ value = value[key]
+ else
+ var sub_query = sub_query_to_s(keys, i)
+ var e = new JsonKeyError("Key `{key}` not found.",
+ self, sub_query)
+ return new JsonValue(e)
+ end
+ else
+ var sub_query = sub_query_to_s(keys, i)
+ var val_type = (new JsonValue(value)).json_type
+ var e = new JsonKeyError("Value at `{sub_query}` is not a map. Got type `{val_type}`",
+ self, sub_query)
+ return new JsonValue(e)
+ end
end
return new JsonValue(value)
end
+
+ # Concatenate all keys up to `last` for debugging purposes.
+ #
+ # Note: This method deletes elements in `keys`.
+ private fun sub_query_to_s(keys: Array[String], last: Int): String do
+ last += 1
+ for j in [last..keys.length[ do keys.pop
+ return keys.join(".")
+ end
+
+ # Return a human-readable description of the type.
+ #
+ # For debugging purpose only.
+ fun json_type: String do
+ if is_array then return "array"
+ if is_bool then return "bool"
+ if is_float then return "float"
+ if is_int then return "int"
+ if is_null then return "null"
+ if is_map then return "map"
+ if is_string then return "string"
+ if is_parse_error then return "parse_error"
+ if is_error then return "error"
+ return "undefined"
+ end
+end
+
+# Keyed access failed.
+class JsonKeyError
+ super Error
+
+ # The value on which the access was requested.
+ var json_value: JsonValue
+
+ # The requested key.
+ #
+ # In the case of `JsonValue.get`, the sub-query that failed.
+ var key: Object
end
redef class Text