///|
/// One JSON object parsed from a physical line in a JSONL stream.
pub(all) struct JsonlRecord {
  line_number : Int
  value : Json
} derive(Debug, Eq, ToJson)

///|
/// A JSONL syntax or record-shape error with its physical source line.
pub(all) suberror JsonlParseError {
  InvalidJson(line_number~ : Int, line~ : String)
  ExpectedObject(line_number~ : Int, line~ : String)
} derive(Debug, Eq)

///|
/// Parses JSONL text into ordered object records while preserving line numbers.
/// Empty and whitespace-only lines are ignored. Every non-empty line must be a
/// complete JSON object.
///
/// # Example
/// ```mbt check
/// test {
///   let records = parse_jsonl("{\"event_id\":\"evt-1\"}\n")
///   assert_eq(records.length(), 1)
///   assert_eq(records[0].line_number, 1)
/// }
/// ```
pub fn parse_jsonl(source : String) -> Array[JsonlRecord] raise JsonlParseError {
  let records : Array[JsonlRecord] = []
  for index, physical_line in source.split("\n") {
    let line = physical_line.trim().to_owned()
    if line != "" {
      let value = @json.parse(line) catch {
        _ => raise JsonlParseError::InvalidJson(line_number=index + 1, line~)
      }
      guard value is { .. } else {
        raise JsonlParseError::ExpectedObject(line_number=index + 1, line~)
      }
      records.push({ line_number: index + 1, value, })
    }
  }
  records
}