///|
pub(all) enum NormalizedConversion {
KeepValue
SignedValue(Int32)
UnsignedValue(Int32)
ScaleValue(Double, Double)
TextValue
BooleanValue
}
///|
pub(all) struct NormalizedField {
source : String
target : String
default_value : Json?
required : Bool
conversion : NormalizedConversion
}
///|
pub(all) struct NormalizedPayload {
value : Json
defaulted : Array[String]
missing : Array[String]
}
///|
pub struct PayloadNormalizer {
fields : Array[NormalizedField]
paths : Array[(Array[String], Array[String])]
}
///|
pub fn payload_normalizer(
fields : Array[NormalizedField],
) -> PayloadNormalizer raise McapError {
let fields = fields.copy()
{ fields, paths: normalized_paths(fields), }
}
///|
pub fn PayloadNormalizer::normalize(
self : PayloadNormalizer,
document : Json,
) -> NormalizedPayload raise McapError {
normalize_prepared(document, self.fields, self.paths)
}
///|
pub fn normalized_field(source : String, target : String) -> NormalizedField {
{
source,
target,
default_value: None,
required: true,
conversion: KeepValue,
}
}
///|
fn normalize_conversion(
value : Json,
conversion : NormalizedConversion,
) -> Json raise McapError {
match conversion {
KeepValue => value
SignedValue(bits) => {
if ![8, 16, 32, 64].contains(bits) {
raise Invalid("invalid normalized integer width")
}
let n = payload_signed(value, bits).reinterpret_as_int64()
Json::number(n.to_double(), repr=n.to_string())
}
UnsignedValue(bits) => {
if ![8, 16, 32, 64].contains(bits) {
raise Invalid("invalid normalized integer width")
}
let n = payload_unsigned(value, bits)
Json::number(n.to_double(), repr=n.to_string())
}
TextValue =>
match value {
String(_) => value
_ => raise Invalid("normalized text field requires a string")
}
BooleanValue =>
match value {
True | False => value
_ => raise Invalid("normalized boolean field requires a boolean")
}
ScaleValue(scale, offset) => {
if scale.is_inf() || scale.is_nan() || offset.is_inf() || offset.is_nan() {
raise Invalid("nonfinite normalization scale or offset")
}
guard value is Number(number, ..) && !number.is_inf() && !number.is_nan() else {
raise Invalid("unit conversion requires a finite JSON number")
}
if compare_decimal(value.stringify(), number.to_string()) != 0 {
raise Invalid("unit conversion would round the input number")
}
let result = number * scale + offset
if result.is_nan() || result.is_inf() {
raise Invalid("unit conversion overflow")
}
Json::number(result)
}
}
}
///|
fn normalized_paths(
fields : Array[NormalizedField],
) -> Array[(Array[String], Array[String])] raise McapError {
if fields.length() > 128 {
raise Invalid("normalization field count exceeds 128")
}
let result : Array[(Array[String], Array[String])] = []
let targets : Array[PayloadEdit] = []
for field in fields {
let source = json_pointer(field.source)
let target = json_pointer(field.target)
if target.is_empty() {
raise Invalid("normalization targets must be object members")
}
// New targets construct objects; array indices are not inferred from names.
result.push((source, target))
targets.push({ path: field.target, replacement: None, })
match field.conversion {
SignedValue(bits) | UnsignedValue(bits) =>
if ![8, 16, 32, 64].contains(bits) {
raise Invalid("invalid normalized integer width")
}
ScaleValue(scale, offset) =>
if scale.is_nan() ||
scale.is_inf() ||
offset.is_nan() ||
offset.is_inf() {
raise Invalid("nonfinite normalization scale or offset")
}
_ => ()
}
if field.default_value is Some(value) {
ignore(normalize_conversion(value, field.conversion))
}
}
ignore(payload_edit_paths(targets))
result
}
///|
fn normalized_insert(
root : Json,
path : Array[String],
position : Int32,
value : Json,
) -> Json raise McapError {
if position == path.length() {
return value
}
guard root is Object(fields) else {
raise Invalid("normalization target collision")
}
let copy : Map[String, Json] = Map([])
for key in fields.keys() {
copy[key] = fields[key]
}
let child = fields.get(path[position]).unwrap_or(Json::object(Map([])))
copy[path[position]] = normalized_insert(child, path, position + 1, value)
Json::object(copy)
}
///|
fn normalize_prepared(
document : Json,
fields : Array[NormalizedField],
paths : Array[(Array[String], Array[String])],
) -> NormalizedPayload raise McapError {
let mut result = Json::object(Map([]))
let defaulted : Array[String] = []
let missing : Array[String] = []
for i = 0; i < fields.length(); i = i + 1 {
let field = fields[i]
let value = match pointer_value(document, paths[i].0) {
Some(value) => value
None =>
match field.default_value {
Some(value) => {
defaulted.push(field.target)
value
}
None => {
if field.required {
raise Invalid(
"required normalization source absent: " + field.source,
)
}
missing.push(field.target)
continue
}
}
}
result = normalized_insert(
result,
paths[i].1,
0,
normalize_conversion(value, field.conversion),
)
}
{ value: result, defaulted, missing, }
}
///|
/// Explicit projection/alias/default/type/unit rules; no implicit schema repair.
pub fn normalize_payload(
document : Json,
fields : Array[NormalizedField],
) -> NormalizedPayload raise McapError {
payload_normalizer(fields).normalize(document)
}
///|
/// Compose with QuerySource.decode_payload before filtering, joining or global
/// pagination. Source declarations are adapted to JSON by that existing adapter.
pub fn normalizing_decoder(
decoder : PayloadDecoder,
fields : Array[NormalizedField],
) -> PayloadDecoder raise McapError {
let normalizer = payload_normalizer(fields)
payload_decoder((schema, channel, bytes) => {
normalizer.normalize((decoder.decode_payload)(schema, channel, bytes)).value
})
}