harn-stdlib 0.8.10

Embedded Harn standard library source catalog
Documentation
// std/coerce — defensive value coercion for unpredictable LLM/JSON output.
//
// Pairs with std/llm/safe's `dict_get_ci` for case-insensitive key lookup.
// Use these helpers when normalizing structured output whose values arrive
// as the "wrong" shape — e.g. a list where you expected a string, a number
// rendered as a string, or a deeply nested dict whose keys disagree on
// casing across model runs.
//
// Import with: import { to_string_lossy, to_string_list, ... } from "std/coerce"
/**
 * Render any value as a single string. Lists join their stringified items
 * with `separator`. Dicts are JSON-stringified. Primitives go through
 * `to_string`. Nil collapses to the empty string.
 */
pub fn to_string_lossy(value: any, separator: string = " ") -> string {
  if value == nil {
    return ""
  }
  let kind = type_of(value)
  if kind == "string" {
    return value
  }
  if kind == "list" {
    var parts = []
    for item in value {
      parts = parts + [to_string_lossy(item, separator)]
    }
    return join(parts, separator)
  }
  if kind == "dict" {
    return json_stringify(value)
  }
  return to_string(value)
}

/**
 * Normalize any value into list<string>. A single string yields a 1-element
 * list (or [] when blank). A list has each element coerced via
 * `to_string_lossy`. A dict becomes its JSON form wrapped in a list. The
 * result is truncated to `max_items`.
 */
pub fn to_string_list(value: any, max_items: int = 256) -> list<string> {
  if value == nil {
    return []
  }
  let kind = type_of(value)
  var raw = []
  if kind == "list" {
    raw = value
  } else if kind == "string" {
    if value != "" {
      raw = [value]
    }
  } else if kind == "dict" {
    raw = [json_stringify(value)]
  } else {
    raw = [to_string(value)]
  }
  var out = []
  var count = 0
  for item in raw {
    if count >= max_items {
      break
    }
    out = out + [to_string_lossy(item)]
    count = count + 1
  }
  return out
}

/** Parse an int-like value, returning fallback on failure. */
pub fn to_int_or(value: any, fallback: int) -> int {
  return to_int(value) ?? fallback
}

/** Parse a float-like value, returning fallback on failure. */
pub fn to_float_or(value: any, fallback: float) -> float {
  return to_float(value) ?? fallback
}

/**
 * Coerce value to bool with a fallback. Recognizes the canonical
 * "true/false/yes/no/y/n/1/0" tokens (case-insensitive) on strings, treats
 * numeric zero as false, and treats empty list/dict as false.
 */
pub fn to_bool_or(value: any, fallback: bool) -> bool {
  if value == nil {
    return fallback
  }
  let kind = type_of(value)
  if kind == "bool" {
    return value
  }
  if kind == "int" || kind == "float" {
    return value != 0
  }
  if kind == "string" {
    let normalized = lowercase(trim(value))
    if normalized == "" {
      return fallback
    }
    if normalized == "true" || normalized == "yes" || normalized == "y" || normalized == "1" {
      return true
    }
    if normalized == "false" || normalized == "no" || normalized == "n" || normalized == "0" {
      return false
    }
    return fallback
  }
  if kind == "list" {
    return len(value) > 0
  }
  if kind == "dict" {
    return len(value.keys()) > 0
  }
  return fallback
}

/**
 * Case-insensitive nested dict lookup. Each path segment matches keys
 * case-insensitively against the current dict layer; returns nil on the
 * first miss or non-dict.
 */
pub fn dict_path_ci(d: any, path: list<string>) -> any {
  var current = d
  for segment in path {
    if type_of(current) != "dict" {
      return nil
    }
    let target = lowercase(to_string(segment))
    var hit = nil
    var found = false
    for k in current.keys() {
      if lowercase(to_string(k)) == target {
        hit = current[k]
        found = true
        break
      }
    }
    if !found {
      return nil
    }
    current = hit
  }
  return current
}

/**
 * Return the first value in `values` that is not nil. If every entry is
 * nil, return `default`.
 */
pub fn first_non_nil(values: list, default: any = nil) -> any {
  for v in values {
    if v != nil {
      return v
    }
  }
  return default
}

/**
 * Return the first non-empty value in `values`. Strings, lists, and dicts
 * must have length > 0; primitives must be non-nil. Returns `default` when
 * nothing qualifies.
 */
pub fn first_present(values: list, default: any = nil) -> any {
  for v in values {
    if v == nil {
      continue
    }
    let kind = type_of(v)
    if kind == "string" {
      if v != "" {
        return v
      }
      continue
    }
    if kind == "list" {
      if len(v) > 0 {
        return v
      }
      continue
    }
    if kind == "dict" {
      if len(v.keys()) > 0 {
        return v
      }
      continue
    }
    return v
  }
  return default
}