harn-stdlib 0.8.24

Embedded Harn standard library source catalog
Documentation
// std/fs - file-system convenience helpers built on the host fs primitives.
import { pretty, safe_parse } from "std/json"

type WriteDataOptions = {pretty?: bool, trailing_newline?: bool, ensure_parent?: bool}

fn __fs_norm(path) -> string {
  return replace(path ?? "", "\\", "/")
}

fn __fs_parent(path) -> string {
  return dirname(path ?? "")
}

fn __fs_with_newline(text, trailing_newline) {
  if !(trailing_newline ?? true) || ends_with(text, "\n") {
    return text
  }
  return text + "\n"
}

/** Create the parent directory for `path` when it is not `.` or empty. */
pub fn ensure_parent_dir(path: string) {
  let parent = __fs_parent(path)
  if parent != "" && parent != "." {
    mkdir(parent)
  }
}

/** Read and parse a JSON file, returning `fallback` for missing or invalid files. */
pub fn read_json(path: string, fallback = nil) {
  if !file_exists(path) {
    return fallback
  }
  let parsed = safe_parse(read_file(path))
  if parsed == nil {
    return fallback
  }
  return parsed
}

/** Read and parse a JSON file, returning `{ok, value?, error?}`. */
pub fn read_json_result(path: string) -> dict {
  if !file_exists(path) {
    return {ok: false, error: "file not found: " + path}
  }
  let parsed = try {
    json_parse(read_file(path))
  }
  if is_ok(parsed) {
    return {ok: true, value: unwrap(parsed)}
  }
  return {ok: false, error: to_string(parsed)}
}

/** Write a JSON file with optional pretty formatting and parent-dir creation. */
pub fn write_json(path: string, value, options: WriteDataOptions = {}) {
  let opts = options ?? {}
  if opts.ensure_parent ?? true {
    ensure_parent_dir(path)
  }
  let body = if opts.pretty ?? false {
    pretty(value)
  } else {
    json_stringify(value)
  }
  write_file(path, __fs_with_newline(body, opts?.trailing_newline ?? true))
}

/** Read and parse a YAML file, returning `fallback` for missing or invalid files. */
pub fn read_yaml(path: string, fallback = nil) {
  if !file_exists(path) {
    return fallback
  }
  let parsed = try {
    yaml_parse(read_file(path))
  }
  if is_ok(parsed) {
    return unwrap(parsed)
  }
  return fallback
}

/** Write a YAML file. */
pub fn write_yaml(path: string, value, options: WriteDataOptions = {}) {
  let opts = options ?? {}
  if opts.ensure_parent ?? true {
    ensure_parent_dir(path)
  }
  write_file(path, __fs_with_newline(yaml_stringify(value), opts?.trailing_newline ?? true))
}

/** Read and parse a TOML file, returning `fallback` for missing or invalid files. */
pub fn read_toml(path: string, fallback = nil) {
  if !file_exists(path) {
    return fallback
  }
  let parsed = try {
    toml_parse(read_file(path))
  }
  if is_ok(parsed) {
    return unwrap(parsed)
  }
  return fallback
}

/** Write a TOML file. */
pub fn write_toml(path: string, value, options: WriteDataOptions = {}) {
  let opts = options ?? {}
  if opts.ensure_parent ?? true {
    ensure_parent_dir(path)
  }
  write_file(path, __fs_with_newline(toml_stringify(value), opts?.trailing_newline ?? true))
}

/** Write a list of lines as a text file. */
pub fn write_lines(path: string, lines: list<string>, options: WriteDataOptions = {}) {
  let opts = options ?? {}
  if opts.ensure_parent ?? true {
    ensure_parent_dir(path)
  }
  write_file(path, __fs_with_newline(join(lines ?? [], "\n"), opts?.trailing_newline ?? true))
}

/** Append one line to a text file. */
pub fn append_line(path: string, line: string) {
  let text = line ?? ""
  append_file(path, text + "\n")
}

/** Create a file if missing and update it to empty content only on first creation. */
pub fn touch(path: string) {
  if !file_exists(path) {
    ensure_parent_dir(path)
    write_file(path, "")
  }
}

/** Return files matching `pattern` below `root`, using the host glob implementation. */
pub fn find_files(root: string, pattern: string, options = {}) -> list<string> {
  let matches = glob(pattern, {base: root, long_running: options?.long_running ?? false})
  if options?.relative ?? false {
    return matches.map({ path -> relative_path(root, path) }).to_list()
  }
  return matches
}

/** Return `path` relative to `root` when it is inside that root. */
pub fn relative_path(root: string, path: string) -> string {
  let base_raw = __fs_norm(root)
  let target = __fs_norm(path)
  let base = if ends_with(base_raw, "/") {
    base_raw
  } else {
    base_raw + "/"
  }
  if starts_with(target, base) {
    return substring(target, len(base))
  }
  return target
}

/** Return whether `path` exists and is a regular file. */
pub fn is_file(path: string) -> bool {
  let info = try {
    stat(path)
  }
  if !is_ok(info) {
    return false
  }
  return unwrap(info)?.is_file ?? false
}

/** Return whether `path` exists and is a directory. */
pub fn is_dir(path: string) -> bool {
  let info = try {
    stat(path)
  }
  if !is_ok(info) {
    return false
  }
  return unwrap(info)?.is_dir ?? false
}

/** Return a file size in bytes, or nil when the path cannot be statted. */
pub fn file_size(path: string) {
  let info = try {
    stat(path)
  }
  if is_ok(info) {
    return unwrap(info)?.size
  }
  return nil
}