///|
priv enum ExtendedJsonMode {
Canonical
Relaxed
}
///|
/// Convert a document to Canonical Extended JSON.
pub fn Document::to_extended_json(self : Document) -> Json raise BsonError {
document_to_extended_json(self, "$", Canonical)
}
///|
pub fn Document::to_extended_json_string(
self : Document,
indent? : Int,
) -> String raise BsonError {
self.to_extended_json().stringify(indent?)
}
///|
/// Convert a document to Relaxed Extended JSON.
pub fn Document::to_relaxed_extended_json(
self : Document,
) -> Json raise BsonError {
document_to_extended_json(self, "$", Relaxed)
}
///|
pub fn Document::to_relaxed_extended_json_string(
self : Document,
indent? : Int,
) -> String raise BsonError {
self.to_relaxed_extended_json().stringify(indent?)
}
///|
pub fn Document::from_extended_json(value : Json) -> Document raise BsonError {
match bson_from_extended_json(value, "$") {
Document(document) => document
_ =>
raise bson_error(
TypeMismatch,
-1,
"$",
"top-level Extended JSON must be a document",
)
}
}
///|
pub fn Document::from_extended_json_string(
value : String,
) -> Document raise BsonError {
let json = @json.parse(value) catch {
_ => raise bson_error(InvalidExtendedJson, -1, "$", "invalid JSON text")
}
Document::from_extended_json(restore_json_number_reprs(json, value))
}
///|
pub fn Bson::to_extended_json(self : Bson) -> Json raise BsonError {
bson_to_extended_json(self, "$", Canonical)
}
///|
/// Convert a BSON value to Relaxed Extended JSON.
pub fn Bson::to_relaxed_extended_json(self : Bson) -> Json raise BsonError {
bson_to_extended_json(self, "$", Relaxed)
}
///|
pub fn Bson::from_extended_json(value : Json) -> Bson raise BsonError {
bson_from_extended_json(value, "$")
}
///|
fn document_to_extended_json(
document : Document,
path : String,
mode : ExtendedJsonMode,
) -> Json raise BsonError {
let values : Array[(String, Json)] = []
for entry in document.to_array() {
let (key, value) = entry
values.push(
(key, bson_to_extended_json(value, field_path(path, key), mode)),
)
}
Json::object(Map(values))
}
///|
fn bson_to_extended_json(
value : Bson,
path : String,
mode : ExtendedJsonMode,
) -> Json raise BsonError {
match value {
Double(value) => double_to_extended_json(value, mode)
String(value) => Json::string(value)
Document(value) => document_to_extended_json(value, path, mode)
Array(values) => {
let result : Array[Json] = []
for index, value in values {
result.push(bson_to_extended_json(value, index_path(path, index), mode))
}
Json::array(result)
}
Binary(value) => {
let inner : Array[(String, Json)] = [
("base64", Json::string(@base64.encode(value.bytes()[:], padding=true))),
("subType", Json::string(value.subtype().to_byte().to_hex())),
]
tagged("$binary", Json::object(Map(inner)))
}
Undefined => tagged("$undefined", Json::boolean(true))
ObjectId(value) => tagged_string("$oid", value.to_hex())
Boolean(value) => Json::boolean(value)
DateTime(value) => datetime_to_extended_json(value, mode)
Null => Json::null()
Regex(value) =>
tagged(
"$regularExpression",
Json::object(
Map([
("pattern", Json::string(value.pattern())),
("options", Json::string(value.options())),
]),
),
)
DbPointer(value) =>
tagged(
"$dbPointer",
Json::object(
Map([
("$ref", Json::string(value.collection())),
("$id", tagged_string("$oid", value.id().to_hex())),
]),
),
)
JavaScript(value) => tagged_string("$code", value)
Symbol(value) => tagged_string("$symbol", value)
JavaScriptWithScope(value) =>
Json::object(
Map([
("$code", Json::string(value.code())),
("$scope", document_to_extended_json(value.scope(), path, mode)),
]),
)
Int32(value) =>
match mode {
Canonical => tagged_string("$numberInt", value.to_string())
Relaxed => Json::number(value.to_double(), repr=value.to_string())
}
Timestamp(value) =>
tagged(
"$timestamp",
Json::object(
Map([
(
"t",
Json::number(
value.time().to_double(),
repr=value.time().to_string(),
),
),
(
"i",
Json::number(
value.increment().to_double(),
repr=value.increment().to_string(),
),
),
]),
),
)
Int64(value) =>
match mode {
Canonical => tagged_string("$numberLong", value.to_string())
Relaxed => Json::number(value.to_double(), repr=value.to_string())
}
Decimal128(value) => tagged_string("$numberDecimal", value.to_string())
MinKey => tagged("$minKey", Json::number(1.0, repr="1"))
MaxKey => tagged("$maxKey", Json::number(1.0, repr="1"))
}
}
///|
fn bson_from_extended_json(value : Json, path : String) -> Bson raise BsonError {
match value {
String(value) => String(value)
Number(value, repr~) => number_from_extended_json(value, repr, path)
True => Boolean(true)
False => Boolean(false)
Null => Null
Array(values) => {
let result : Array[Bson] = []
for index, value in values {
result.push(bson_from_extended_json(value, index_path(path, index)))
}
Array(result)
}
Object(values) => object_from_extended_json(values, path)
}
}
///|
fn object_from_extended_json(
values : Map[String, Json],
path : String,
) -> Bson raise BsonError {
if values.length() == 1 {
match values.to_array() {
[("$numberInt", String(value))] => Int32(parse_int(value, path))
[("$numberLong", String(value))] => Int64(parse_int64(value, path))
[("$numberDouble", String(value))] => Double(parse_double(value, path))
[("$numberDecimal", String(value))] =>
Decimal128(
Decimal128::from_string(value) catch {
_ => invalid_extended(path, "invalid $numberDecimal")
},
)
[("$oid", String(value))] => ObjectId(ObjectId::from_hex(value))
[("$binary", Object(value))] => Binary(binary_from_json(value, path))
[("$uuid", String(value))] => Binary(uuid_binary_from_json(value, path))
[("$date", Object(value))] =>
DateTime(DateTime::from_millis(date_from_json(value, path)))
[("$date", String(value))] =>
DateTime(DateTime::from_millis(parse_rfc3339_millis(value, path)))
[("$regularExpression", Object(value))] =>
Regex(regex_from_json(value, path))
[("$timestamp", Object(value))] =>
Timestamp(timestamp_from_json(value, path))
[("$minKey", Number(value, ..))] if value == 1.0 => MinKey
[("$maxKey", Number(value, ..))] if value == 1.0 => MaxKey
[("$undefined", True)] => Undefined
[("$symbol", String(value))] => Symbol(value)
[("$code", String(value))] => JavaScript(value)
[("$dbPointer", Object(value))] =>
DbPointer(db_pointer_from_json(value, path))
[(key, _)] if is_extended_json_wrapper_key(key) =>
invalid_extended(path, "invalid Extended JSON wrapper " + key)
_ => plain_document_from_json(values, path)
}
} else if values.length() == 2 {
match (values.get("$code"), values.get("$scope")) {
(Some(String(code)), Some(Object(scope))) =>
JavaScriptWithScope(
JavaScriptWithScope::new(
code,
plain_document_from_json_value(scope, field_path(path, "$scope")),
),
)
_ =>
if contains_extended_json_wrapper_key(values) {
invalid_extended(
path, "Extended JSON wrapper contains invalid fields",
)
} else {
plain_document_from_json(values, path)
}
}
} else if contains_extended_json_wrapper_key(values) {
invalid_extended(path, "Extended JSON wrapper contains invalid fields")
} else {
plain_document_from_json(values, path)
}
}
///|
fn plain_document_from_json(
values : Map[String, Json],
path : String,
) -> Bson raise BsonError {
Document(plain_document_from_json_value(values, path))
}
///|
fn plain_document_from_json_value(
values : Map[String, Json],
path : String,
) -> Document raise BsonError {
let document = Document::new()
for entry in values.to_array() {
let (key, value) = entry
if key.contains("\u0000") {
return invalid_extended(path, "Extended JSON document key contains NUL")
}
document.set(key, bson_from_extended_json(value, field_path(path, key)))
|> ignore
}
document
}
///|
fn binary_from_json(
values : Map[String, Json],
path : String,
) -> Binary raise BsonError {
if values.length() != 2 {
return invalid_extended(path, "$binary contains unexpected fields")
}
match (values.get("base64"), values.get("subType")) {
(Some(String(encoded)), Some(String(subtype))) => {
let bytes = @base64.decode(encoded) catch {
_ =>
raise bson_error(
InvalidExtendedJson,
-1,
path,
"invalid Extended JSON base64",
)
}
Binary::new(
BinarySubtype::from_byte(parse_hex_byte(subtype, path)),
bytes,
)
}
_ => invalid_extended(path, "$binary requires base64 and subType strings")
}
}
///|
fn uuid_binary_from_json(
value : String,
path : String,
) -> Binary raise BsonError {
Uuid::from_string(value).to_binary() catch {
_ => invalid_extended(path, "invalid $uuid")
}
}
///|
fn date_from_json(
values : Map[String, Json],
path : String,
) -> Int64 raise BsonError {
match values.to_array() {
[("$numberLong", String(value))] => parse_int64(value, path)
_ => invalid_extended(path, "canonical $date requires $numberLong")
}
}
///|
fn regex_from_json(
values : Map[String, Json],
path : String,
) -> Regex raise BsonError {
if values.length() != 2 {
return invalid_extended(
path, "$regularExpression contains unexpected fields",
)
}
match (values.get("pattern"), values.get("options")) {
(Some(String(pattern)), Some(String(options))) => {
if pattern.contains("\u0000") {
raise bson_error(InvalidRegex, -1, path, "invalid Extended JSON regex")
}
match normalize_regex_options(options) {
Some(normalized) => Regex::new(pattern, normalized)
None =>
raise bson_error(
InvalidRegex,
-1,
path,
"invalid Extended JSON regex",
)
}
}
_ =>
invalid_extended(path, "$regularExpression requires pattern and options")
}
}
///|
fn timestamp_from_json(
values : Map[String, Json],
path : String,
) -> Timestamp raise BsonError {
match (values.get("t"), values.get("i")) {
(Some(Number(time, ..)), Some(Number(increment, ..))) if values.length() ==
2 =>
Timestamp::new(
timestamp_component(time, path),
timestamp_component(increment, path),
)
_ => invalid_extended(path, "$timestamp requires numeric t and i")
}
}
///|
fn db_pointer_from_json(
values : Map[String, Json],
path : String,
) -> DbPointer raise BsonError {
if values.length() != 2 {
return invalid_extended(path, "$dbPointer contains unexpected fields")
}
match (values.get("$ref"), values.get("$id")) {
(Some(String(collection)), Some(Object(id))) =>
match id.to_array() {
[("$oid", String(hex))] =>
DbPointer::new(collection, ObjectId::from_hex(hex))
_ => invalid_extended(path, "$dbPointer.$id requires $oid")
}
_ => invalid_extended(path, "$dbPointer requires $ref and $id")
}
}
///|
fn tagged(key : String, value : Json) -> Json {
Json::object(Map([(key, value)]))
}
///|
fn tagged_string(key : String, value : String) -> Json {
tagged(key, Json::string(value))
}
///|
fn double_to_extended_json(value : Double, mode : ExtendedJsonMode) -> Json {
let text = canonical_double(value)
match mode {
Relaxed if !value.is_nan() && !value.is_inf() =>
Json::number(value, repr=text)
_ => tagged_string("$numberDouble", text)
}
}
///|
fn datetime_to_extended_json(value : DateTime, mode : ExtendedJsonMode) -> Json {
let milliseconds = value.to_millis()
match mode {
Relaxed =>
match relaxed_datetime_string(milliseconds) {
Some(text) => tagged_string("$date", text)
None =>
tagged(
"$date",
tagged_string("$numberLong", milliseconds.to_string()),
)
}
Canonical =>
tagged("$date", tagged_string("$numberLong", milliseconds.to_string()))
}
}
///|
fn canonical_double(value : Double) -> String {
if value.is_nan() {
"NaN"
} else if value.is_pos_inf() {
"Infinity"
} else if value.is_neg_inf() {
"-Infinity"
} else if value.reinterpret_as_int64() == -9223372036854775807L - 1L {
"-0.0"
} else {
let text = value.to_string()
if text.contains(".") || text.contains("e") || text.contains("E") {
text
} else {
text + ".0"
}
}
}
///|
fn number_from_extended_json(
value : Double,
repr : String?,
path : String,
) -> Bson raise BsonError {
match repr {
Some(text) if !text.contains(".") &&
!text.contains("e") &&
!text.contains("E") =>
match parse_relaxed_integer(text) {
Some(integer) if integer >= -2147483648L && integer <= 2147483647L =>
Int32(integer.to_int())
Some(integer) => Int64(integer)
None => Double(value)
}
None if !value.is_nan() &&
!value.is_inf() &&
value.floor() == value &&
value >= -9007199254740991.0 &&
value <= 9007199254740991.0 => {
let integer = value.to_int64()
if integer >= -2147483648L && integer <= 2147483647L {
Int32(integer.to_int())
} else {
Int64(integer)
}
}
_ =>
if value.is_nan() || value.is_inf() {
invalid_extended(path, "JSON number is outside the finite Double range")
} else {
Double(value)
}
}
}
///|
fn parse_relaxed_integer(value : String) -> Int64? {
try @strconv.from_str(value) catch {
_ => None
} noraise {
integer => Some(integer)
}
}
///|
fn is_extended_json_wrapper_key(key : String) -> Bool {
match key {
"$numberInt"
| "$numberLong"
| "$numberDouble"
| "$numberDecimal"
| "$oid"
| "$binary"
| "$uuid"
| "$date"
| "$regularExpression"
| "$timestamp"
| "$minKey"
| "$maxKey"
| "$undefined"
| "$symbol"
| "$code"
| "$dbPointer" => true
_ => false
}
}
///|
fn contains_extended_json_wrapper_key(values : Map[String, Json]) -> Bool {
for key in values.keys() {
if is_extended_json_wrapper_key(key) {
return true
}
}
false
}
///|
fn parse_int(value : String, path : String) -> Int raise BsonError {
@strconv.from_str(value) catch {
_ => invalid_extended(path, "invalid $numberInt")
}
}
///|
fn parse_int64(value : String, path : String) -> Int64 raise BsonError {
@strconv.from_str(value) catch {
_ => invalid_extended(path, "invalid $numberLong")
}
}
///|
fn parse_double(value : String, path : String) -> Double raise BsonError {
match value {
"NaN" => @double.not_a_number
"Infinity" => @double.infinity
"-Infinity" => @double.neg_infinity
_ =>
@strconv.from_str(value) catch {
_ => invalid_extended(path, "invalid $numberDouble")
}
}
}
///|
fn parse_hex_byte(value : String, path : String) -> Byte raise BsonError {
match value.length() {
1 =>
match hex_nibble(value[0]) {
Some(low) => low.to_byte()
None => invalid_extended(path, "binary subType contains non-hex digits")
}
2 =>
match (hex_nibble(value[0]), hex_nibble(value[1])) {
(Some(high), Some(low)) => (high * 16 + low).to_byte()
_ => invalid_extended(path, "binary subType contains non-hex digits")
}
_ =>
invalid_extended(
path, "binary subType must contain one or two hex digits",
)
}
}
///|
fn timestamp_component(value : Double, path : String) -> UInt raise BsonError {
if value.is_nan() ||
value.is_inf() ||
value < 0.0 ||
value > 4294967295.0 ||
value.floor() != value {
return invalid_extended(
path, "$timestamp components must be unsigned 32-bit integers",
)
}
UInt::trunc_double(value)
}
///|
fn[T] invalid_extended(path : String, message : String) -> T raise BsonError {
raise bson_error(InvalidExtendedJson, -1, path, message)
}