///|
/// Validate an in-memory JSON tree, not JSON text. The root has depth zero;
/// values reached by at most 64 object/array edges are accepted. Cycles are
/// checked by container identity on the active path, so shared acyclic subtrees
/// remain legal. Non-finite numbers and malformed numeric repr are rejected.
pub fn validate_json(value : Json) -> Result[Unit, SdkError] {
validate_json_at(value, 0, [])
}
///|
fn validate_json_at(
value : Json,
depth : Int32,
ancestors : Array[Json],
) -> Result[Unit, SdkError] {
if depth > 64 {
return Err(SdkError::new(InvalidContext, "JSON depth exceeds 64"))
}
match value {
Number(number, repr~) => {
if number.is_nan() || number.is_inf() {
return Err(SdkError::new(InvalidContext, "JSON number must be finite"))
}
if repr is Some(text) && !valid_number_repr(text) {
return Err(
SdkError::new(InvalidContext, "invalid JSON number representation"),
)
}
Ok(())
}
Array(items) => {
for ancestor in ancestors {
if ancestor is Array(previous) && physical_equal(items, previous) {
return Err(SdkError::new(InvalidContext, "cyclic JSON array"))
}
}
ancestors.push(value)
for item in items {
match validate_json_at(item, depth + 1, ancestors) {
Err(error) => {
ignore(ancestors.pop())
return Err(error)
}
Ok(_) => ()
}
}
ignore(ancestors.pop())
Ok(())
}
Object(fields) => {
for ancestor in ancestors {
if ancestor is Object(previous) && physical_equal(fields, previous) {
return Err(SdkError::new(InvalidContext, "cyclic JSON object"))
}
}
ancestors.push(value)
for _, item in fields {
match validate_json_at(item, depth + 1, ancestors) {
Err(error) => {
ignore(ancestors.pop())
return Err(error)
}
Ok(_) => ()
}
}
ignore(ancestors.pop())
Ok(())
}
Null | True | False | String(_) => Ok(())
}
}
///|
/// Json permits a raw numeric repr used verbatim by stringification. Check its
/// grammar without converting to Double: conversion would lose Int64 precision.
fn valid_number_repr(text : String) -> Bool {
let chars = text.to_array()
let length = chars.length()
let mut index = 0
if index < length && chars[index] == '-' {
index += 1
}
if index >= length {
return false
}
if chars[index] == '0' {
index += 1
} else if chars[index] >= '1' && chars[index] <= '9' {
while index < length && chars[index] >= '0' && chars[index] <= '9' {
index += 1
}
} else {
return false
}
if index < length && chars[index] == '.' {
index += 1
let start = index
while index < length && chars[index] >= '0' && chars[index] <= '9' {
index += 1
}
if index == start {
return false
}
}
if index < length && (chars[index] == 'e' || chars[index] == 'E') {
index += 1
if index < length && (chars[index] == '+' || chars[index] == '-') {
index += 1
}
let start = index
while index < length && chars[index] >= '0' && chars[index] <= '9' {
index += 1
}
if index == start {
return false
}
}
index == length
}
///|
/// Deep-copy a validated JSON tree, preserving numeric repr and all keys.
/// Validate first when errors must be reported. Invalid inputs fail closed to
/// null rather than recursing forever or returning an aliased mutable value.
pub fn copy_json(value : Json) -> Json {
match validate_json(value) {
Ok(_) => copy_validated_json(value, 0)
Err(_) => Json::null()
}
}
///|
fn copy_validated_json(value : Json, depth : Int32) -> Json {
// Defensive bound: even this private copier terminates on unvalidated input.
if depth > 64 {
return Json::null()
}
match value {
Null => Json::null()
True => Json::boolean(true)
False => Json::boolean(false)
String(text) => Json::string(text)
Number(number, repr~) =>
match repr {
Some(text) => Json::number(number, repr=text)
None => Json::number(number)
}
Array(items) => {
let copied = []
for item in items {
copied.push(copy_validated_json(item, depth + 1))
}
Json::array(copied)
}
Object(fields) => {
let copied = Map([])
for key, item in fields {
copied.set(key, copy_validated_json(item, depth + 1))
}
Json::object(copied)
}
}
}
///|
/// The wire format is a flat object; targetingKey is a reserved wire field.
/// Attribute keys are unrestricted in memory. If an attribute has that spelling
/// and a targeting key is present, the targeting key wins in the wire object.
/// from_json always interprets that field as the targeting key, not an attribute.
pub fn EvaluationContext::to_json(self : EvaluationContext) -> Json {
let fields = self.attributes()
match self.targeting_key() {
Some(key) => fields.set("targetingKey", Json::string(key))
None => ()
}
Json::object(fields)
}
///|
/// Read an already parsed JSON object. The reserved targetingKey, when present,
/// must be a string (null is not equivalent to absence). Other fields are copied
/// as attributes. No string parsing, coercion or recursive object merging occurs.
pub fn EvaluationContext::from_json(
value : Json,
) -> Result[EvaluationContext, SdkError] {
guard value is Object(fields) else {
return Err(
SdkError::new(InvalidContext, "evaluation context must be a JSON object"),
)
}
match validate_json(value) {
Err(error) => return Err(error)
Ok(_) => ()
}
let mut targeting_key : String? = None
let attributes = Map([])
for key, item in fields {
if key == "targetingKey" {
guard item is String(text) else {
return Err(
SdkError::new(InvalidContext, "targetingKey must be a string"),
)
}
targeting_key = Some(text)
} else {
attributes.set(key, copy_json(item))
}
}
// Both maps and targeting key are locally owned; avoid another envelope copy.
Ok({ targeting_key, attributes, })
}