harn-stdlib 0.8.23

Embedded Harn standard library source catalog
Documentation
// std/jsonl — JSON-lines stream helpers (read/parse/write/append).
//
// Replaces the recurring "read_file → split('\n') → trim → skip blanks
// → json_parse with try/is_ok" dance every harness writes when consuming
// transcript / event-log files. All readers default to lenient mode
// (skip parse errors) so a single malformed line doesn't poison the
// run; flip `strict: true` to surface them instead.
//
// Import with: import { read_jsonl, parse_jsonl, write_jsonl, append_jsonl } from "std/jsonl"
/** Lenient-by-default options for the JSONL readers. */
type ReadJsonlOptions = {strict?: bool, max_records?: int, on_error?: any}

fn __read_options(options) {
  let opts = options ?? {}
  return {
    strict: opts?.strict ?? false,
    max_records: to_int(opts?.max_records) ?? 0,
    on_error: opts?.on_error ?? nil,
  }
}

fn __parse_line(text: string, opts, out_in: list, dropped_in: int) -> dict {
  let trimmed = trim(text)
  if trimmed == "" {
    return {out: out_in, dropped: dropped_in, halt: false}
  }
  let parsed = try {
    json_parse(trimmed)
  }
  if is_ok(parsed) {
    return {out: out_in + [unwrap(parsed)], dropped: dropped_in, halt: false}
  }
  if opts.strict {
    throw {error: "json_parse_failed", line: trimmed, reason: unwrap_err(parsed)}
  }
  if opts.on_error != nil {
    opts.on_error(trimmed, unwrap_err(parsed))
  }
  return {out: out_in, dropped: dropped_in + 1, halt: false}
}

/**
 * Parse a JSONL string into list<dict>. Blank lines are skipped; malformed
 * lines are dropped silently unless `strict: true`. When `max_records > 0`,
 * parsing stops after that many successes.
 */
pub fn parse_jsonl(text: string, options: ReadJsonlOptions = {}) -> list {
  let opts = __read_options(options)
  let body = text ?? ""
  if body == "" {
    return []
  }
  var out = []
  var dropped = 0
  for line in split(body, "\n") {
    if opts.max_records > 0 && len(out) >= opts.max_records {
      break
    }
    let outcome = __parse_line(line, opts, out, dropped)
    out = outcome.out
    dropped = outcome.dropped
  }
  return out
}

/**
 * Read a JSONL file from disk and return list<dict>. Missing files yield
 * the empty list (use `file_exists` first if you need to distinguish
 * absent from empty). Pass `strict: true` to surface parse errors.
 */
pub fn read_jsonl(path: string, options: ReadJsonlOptions = {}) -> list {
  if !file_exists(path) {
    return []
  }
  let raw = try {
    read_file(path)
  }
  if !is_ok(raw) {
    if (options ?? {}).strict {
      throw {error: "read_failed", path: path, reason: unwrap_err(raw)}
    }
    return []
  }
  return parse_jsonl(unwrap(raw), options)
}

fn __serialize(items: list) -> string {
  var lines = []
  for item in items ?? [] {
    lines = lines + [json_stringify(item)]
  }
  return join(lines, "\n") + "\n"
}

/**
 * Write a list of items to `path` as JSON-lines. The file is fully
 * replaced. Returns the count of items written.
 */
pub fn write_jsonl(path: string, items: list) -> int {
  let body = __serialize(items)
  write_file(path, body)
  return len(items ?? [])
}

/**
 * Append a single item to `path` as a JSONL record. Creates the file if
 * absent. Returns the byte count appended (newline included).
 */
pub fn append_jsonl(path: string, item) -> int {
  let line = json_stringify(item) + "\n"
  append_file(path, line)
  return len(line)
}

/**
 * Stream-fold over JSONL content. `reducer` is invoked as
 * `reducer(state, record, index)` for each successfully parsed record;
 * the final state is returned. Replaces the bespoke `for event in events
 * { state = fold(state, event) }` loops harness authors hand-write.
 */
pub fn fold_jsonl(text: string, initial, reducer, options: ReadJsonlOptions = {}) -> any {
  let opts = __read_options(options)
  var state = initial
  var index = 0
  let body = text ?? ""
  if body == "" {
    return state
  }
  for line in split(body, "\n") {
    let trimmed = trim(line)
    if trimmed == "" {
      continue
    }
    if opts.max_records > 0 && index >= opts.max_records {
      break
    }
    let parsed = try {
      json_parse(trimmed)
    }
    if is_ok(parsed) {
      state = reducer(state, unwrap(parsed), index)
      index = index + 1
      continue
    }
    if opts.strict {
      throw {error: "json_parse_failed", line: trimmed, reason: unwrap_err(parsed)}
    }
    if opts.on_error != nil {
      opts.on_error(trimmed, unwrap_err(parsed))
    }
  }
  return state
}