///|
/// Summary information for a collection of Ion values.
pub(all) struct IonStats {
mut value_count : Int
mut max_depth : Int
mut max_container_items : Int
mut symbol_count : Int
mut text_bytes : Int
mut blob_bytes : Int
} derive(Debug, Eq)
///|
/// Analyze value count, nesting, symbols, and payload sizes before encoding.
pub fn analyze(values : ArrayView[@model.IonValue]) -> IonStats {
let stats = {
value_count: 0,
max_depth: 0,
max_container_items: 0,
symbol_count: 0,
text_bytes: 0,
blob_bytes: 0,
}
for value in values {
stats.observe(value, 1)
}
stats
}
///|
/// Return the number of Ion values, excluding annotation wrappers.
pub fn IonStats::value_count(self : IonStats) -> Int {
self.value_count
}
///|
/// Return the deepest structural level encountered by the analysis.
pub fn IonStats::max_depth(self : IonStats) -> Int {
self.max_depth
}
///|
/// Return the largest element, field, or annotation count in one container.
pub fn IonStats::max_container_items(self : IonStats) -> Int {
self.max_container_items
}
///|
/// Return the number of symbol occurrences, including field names and annotations.
pub fn IonStats::symbol_count(self : IonStats) -> Int {
self.symbol_count
}
///|
/// Return the combined UTF-8 and blob/clob payload size in bytes.
pub fn IonStats::payload_bytes(self : IonStats) -> Int {
self.text_bytes + self.blob_bytes
}
///|
/// Check the measured value, depth, container, symbol, text, and binary budgets.
pub fn IonStats::fits(self : IonStats, limits : @model.Limits) -> Bool {
self.first_exceeded_limit(limits) is None
}
///|
/// Return the first measured resource budget that this document exceeds.
///
/// `TotalBytes` is intentionally omitted: the final encoded size depends on
/// text escaping, binary length encoding, and output options, and is checked
/// by the encoder after it has produced the representation.
pub fn IonStats::first_exceeded_limit(
self : IonStats,
limits : @model.Limits,
) -> @model.LimitKind? {
if self.value_count > limits.max_values() {
Some(@model.LimitKind::Values)
} else if self.max_depth > limits.max_depth() {
Some(@model.LimitKind::Depth)
} else if self.max_container_items > limits.max_container_items() {
Some(@model.LimitKind::ContainerItems)
} else if self.symbol_count > limits.max_symbols() {
Some(@model.LimitKind::SymbolCount)
} else if self.text_bytes > limits.max_text_bytes() {
Some(@model.LimitKind::TextBytes)
} else if self.blob_bytes > limits.max_blob_bytes() {
Some(@model.LimitKind::BlobBytes)
} else {
None
}
}
///|
/// Analyze a document and reject the first measured resource budget it exceeds.
///
/// This is suitable immediately before writing an application-owned value
/// document. It preserves the `IonError::Limit` category and its `LimitKind`;
/// encoded byte size remains checked by `encode_text` or `encode_binary`.
///
/// # Example
/// ```mbt check
/// test {
/// let values = [@model.IonValue::string("ion")]
/// let stats = preflight_values(
/// values[:],
/// limits=@model.Limits::new(max_text_bytes=3),
/// )
/// assert_eq(stats.value_count(), 1)
/// }
/// ```
pub fn preflight_values(
values : ArrayView[@model.IonValue],
limits? : @model.Limits,
) -> IonStats raise @model.IonError {
let stats = analyze(values)
let limits = limits.unwrap_or(@model.Limits::default())
match stats.first_exceeded_limit(limits) {
Some(kind) =>
raise @model.IonError::Limit(
kind~,
offset=0,
line=1,
column=1,
message=preflight_limit_message(kind),
)
None => stats
}
}
///|
fn IonStats::observe(
self : IonStats,
value : @model.IonValue,
depth : Int,
) -> Unit {
match value {
@model.IonValue::Annotated(annotations, inner) => {
self.record_container_items(annotations.length())
self.symbol_count += annotations.length()
for annotation in annotations {
match annotation.text_value() {
Some(text) => self.text_bytes += @utf8.encode(text).length()
None => ()
}
}
self.observe(inner, depth)
}
@model.IonValue::Symbol(symbol) => {
self.record_value(depth)
self.symbol_count += 1
self.record_symbol_text(symbol)
}
@model.IonValue::String(text) => {
self.record_value(depth)
self.text_bytes += @utf8.encode(text).length()
}
@model.IonValue::Blob(bytes) | @model.IonValue::Clob(bytes) => {
self.record_value(depth)
self.blob_bytes += bytes.length()
}
@model.IonValue::List(values) | @model.IonValue::Sexp(values) => {
self.record_value(depth)
self.record_container_items(values.length())
for child in values {
self.observe(child, depth + 1)
}
}
@model.IonValue::Struct(fields) => {
self.record_value(depth)
self.record_container_items(fields.length())
for field in fields {
self.symbol_count += 1
self.record_symbol_text(field.0)
self.observe(field.1, depth + 1)
}
}
_ => self.record_value(depth)
}
}
///|
fn IonStats::record_value(self : IonStats, depth : Int) -> Unit {
self.value_count += 1
if depth > self.max_depth {
self.max_depth = depth
}
}
///|
fn IonStats::record_container_items(self : IonStats, count : Int) -> Unit {
if count > self.max_container_items {
self.max_container_items = count
}
}
///|
fn IonStats::record_symbol_text(
self : IonStats,
symbol : @model.IonSymbol,
) -> Unit {
match symbol.text_value() {
Some(text) => self.text_bytes += @utf8.encode(text).length()
None => ()
}
}
///|
fn preflight_limit_message(kind : @model.LimitKind) -> String {
match kind {
@model.LimitKind::Values => "document exceeds max_values"
@model.LimitKind::Depth => "document exceeds max_depth"
@model.LimitKind::ContainerItems => "document exceeds max_container_items"
@model.LimitKind::TextBytes => "document exceeds max_text_bytes"
@model.LimitKind::BlobBytes => "document exceeds max_blob_bytes"
@model.LimitKind::SymbolCount => "document exceeds max_symbols"
@model.LimitKind::TotalBytes => "document exceeds max_total_bytes"
}
}