///|
pub fn protobuf_json_options() -> ProtobufOptions {
{ ..protobuf_options(), protojson: true, int64_as_string: true, }
}
///|
fn proto_integer(value : Json) -> Int64 raise McapError {
let text = match value {
String(text) => text
Number(_, ..) => value.stringify()
_ => raise Invalid("invalid protobuf integer")
}
let negative = text.has_prefix("-")
let digits = if negative { text[1:].to_string() } else { text }
let magnitude = parse_decimal(digits)
if magnitude >
(if negative { 9223372036854775808UL } else { 9223372036854775807UL }) {
raise Invalid("protobuf integer exceeds Int64")
}
(if negative { 0UL - magnitude } else { magnitude }).to_int64()
}
///|
fn proto_pad(value : Int, width : Int) -> String {
let text = value.to_string()
let out = StringBuilder::new()
for i = text.length(); i < width; i = i + 1 {
out.write_char('0')
}
out.write_string(text)
out.to_string()
}
///|
fn proto_fraction(nanos : Int) -> String {
if nanos == 0 {
return ""
}
let text = proto_pad(if nanos < 0 { -nanos } else { nanos }, 9)
"." +
text[0:if nanos % 1000000 == 0 {
3
} else if nanos % 1000 == 0 {
6
} else {
9
}].to_string()
}
///|
fn proto_year_days(year : Int) -> Int {
let previous = year - 1
365 * previous + previous / 4 - previous / 100 + previous / 400
}
///|
fn proto_timestamp(seconds : Int64, nanos : Int64) -> String raise McapError {
if seconds < -62135596800L ||
seconds > 253402300799L ||
nanos < 0L ||
nanos > 999999999L {
raise Invalid("invalid protobuf Timestamp range")
}
let mut days = seconds / 86400L
let mut within = seconds % 86400L
if within < 0L {
days -= 1L
within += 86400L
}
let ordinal = days.to_int() + 719162
let mut low = 1
let mut high = 10000
while low + 1 < high {
let mid = (low + high) / 2
if proto_year_days(mid) <= ordinal {
low = mid
} else {
high = mid
}
}
let mut remaining = ordinal - proto_year_days(low)
let lengths = [
31,
if low % 4 == 0 && (low % 100 != 0 || low % 400 == 0) {
29
} else {
28
},
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
]
let mut month = 0
while remaining >= lengths[month] {
remaining -= lengths[month]
month += 1
}
let clock = within.to_int()
"\{proto_pad(low,4)}-\{proto_pad(month + 1,2)}-\{proto_pad(remaining + 1,2)}T\{proto_pad(clock / 3600,2)}:\{proto_pad(clock / 60 % 60,2)}:\{proto_pad(clock % 60,2)}\{proto_fraction(nanos.to_int())}Z"
}
///|
fn proto_special(name : String) -> Bool {
[
"Timestamp", "Duration", "Any", "FieldMask", "Struct", "Value", "ListValue",
"DoubleValue", "FloatValue", "Int64Value", "UInt64Value", "Int32Value", "UInt32Value",
"BoolValue", "StringValue", "BytesValue",
].any(fn(short) { name == "google.protobuf." + short })
}
///|
fn ProtobufDecoder::well_known(
self : ProtobufDecoder,
name : String,
fields : Map[String, Json],
budget : Array[Int],
depth : Int,
) -> Json raise McapError {
let model = self.messages[name]
fn require(
number : Int,
kind : Int,
label : Int,
json_name : String,
) -> Unit raise McapError {
guard model.fields.get(number) is Some(field) &&
field.kind == kind &&
field.label == label &&
field.json_name == json_name else {
raise Invalid("invalid well-known protobuf descriptor")
}
}
if proto_special(name) {
match name {
"google.protobuf.Timestamp" | "google.protobuf.Duration" => {
require(1, 3, 1, "seconds")
require(2, 5, 1, "nanos")
}
"google.protobuf.Any" => {
require(1, 9, 1, "typeUrl")
require(2, 12, 1, "value")
}
"google.protobuf.Struct" => require(1, 11, 3, "fields")
"google.protobuf.ListValue" => require(1, 11, 3, "values")
"google.protobuf.FieldMask" => require(1, 9, 3, "paths")
"google.protobuf.Value" =>
for
(number, kind, json_name) in [
(1, 14, "nullValue"),
(2, 1, "numberValue"),
(3, 9, "stringValue"),
(4, 8, "boolValue"),
(5, 11, "structValue"),
(6, 11, "listValue"),
] {
require(number, kind, 1, json_name)
}
_ => {
let kind = match name {
"google.protobuf.DoubleValue" => 1
"google.protobuf.FloatValue" => 2
"google.protobuf.Int64Value" => 3
"google.protobuf.UInt64Value" => 4
"google.protobuf.Int32Value" => 5
"google.protobuf.UInt32Value" => 13
"google.protobuf.BoolValue" => 8
"google.protobuf.StringValue" => 9
_ => 12
}
require(1, kind, 1, "value")
}
}
}
fn integer(key : String) -> Int64 raise McapError {
match fields.get(key) {
None => 0L
Some(value) => proto_integer(value)
}
}
fn text(key : String) -> String raise McapError {
match fields.get(key) {
None => ""
Some(String(value)) => value
_ => raise Invalid("invalid protobuf special string")
}
}
match name {
"google.protobuf.Timestamp" =>
Json::string(proto_timestamp(integer("seconds"), integer("nanos")))
"google.protobuf.Duration" => {
let seconds = integer("seconds")
let nanos = integer("nanos")
if seconds < -315576000000L ||
seconds > 315576000000L ||
nanos < -999999999L ||
nanos > 999999999L ||
(seconds < 0L && nanos > 0L) ||
(seconds > 0L && nanos < 0L) {
raise Invalid("invalid protobuf Duration range or sign")
}
Json::string(
(if seconds < 0L || nanos < 0L { "-" } else { "" }) +
(if seconds < 0L { -seconds } else { seconds }).to_string() +
proto_fraction(nanos.to_int()) +
"s",
)
}
"google.protobuf.DoubleValue"
| "google.protobuf.FloatValue"
| "google.protobuf.Int64Value"
| "google.protobuf.UInt64Value"
| "google.protobuf.Int32Value"
| "google.protobuf.UInt32Value"
| "google.protobuf.BoolValue"
| "google.protobuf.StringValue"
| "google.protobuf.BytesValue" => {
let model = self.messages[name]
guard model.fields.get(1) is Some(field) && field.json_name == "value" else {
raise Invalid("invalid protobuf wrapper descriptor")
}
fields.get_or_default("value", self.default_value(field))
}
"google.protobuf.Struct" =>
fields.get_or_default("fields", Json::object(Map([])))
"google.protobuf.ListValue" =>
fields.get_or_default("values", Json::array([]))
"google.protobuf.Value" => {
if fields.length() != 1 {
raise Invalid("protobuf Value requires one selected kind")
}
let (kind, value) = fields.iter().next().unwrap()
if kind == "nullValue" {
Json::null()
} else if kind == "numberValue" {
guard value is Number(_, ..) else {
raise Invalid("protobuf Value cannot represent nonfinite numbers")
}
value
} else {
value
}
}
"google.protobuf.FieldMask" => {
let paths = match fields.get("paths") {
None => []
Some(Array(paths)) => paths
_ => raise Invalid("invalid protobuf FieldMask")
}
let names = []
for path in paths {
guard path is String(path) else {
raise Invalid("invalid protobuf field path")
}
let mut underscore = false
for char in path {
let n = char.to_int()
if (n >= 65 && n <= 90) || (underscore && (n < 97 || n > 122)) {
raise Invalid("FieldMask cannot convert path to camel case")
}
underscore = char == '_'
}
if underscore {
raise Invalid("FieldMask ends in underscore")
}
names.push(proto_json_name(path))
}
Json::string(names.join(","))
}
"google.protobuf.Any" => {
let url = text("typeUrl")
let data = text("value")
if url == "" && data == "" {
return Json::object(Map([]))
}
let parts = url.split("/").to_array()
if parts.length() < 2 || parts[parts.length() - 1] == "" {
raise Invalid("invalid protobuf Any type URL")
}
let target = parts[parts.length() - 1].to_string()
let bytes = @base64.decode(data[:]) catch {
_ => raise Invalid("invalid protobuf Any bytes")
}
let value = self.message(target, bytes, budget, depth + 1)
if proto_special(target) {
{ "@type": Json::string(url), "value": value }
} else {
guard value is Object(object) else {
raise Invalid("protobuf Any target must be an object")
}
if object.contains("@type") {
raise Invalid("protobuf Any target collides with @type")
}
object["@type"] = Json::string(url)
Json::object(object)
}
}
_ => Json::object(fields)
}
}