Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-environment

Environment files for Pudu: load .env variables safely with layered files, discovery, expansion, typed reads, and secrets kept out of every message

0.1.0Apache-2.01

InstallClose

Discovery.pudu

Pudu103 lines4.1 KB

GitHub ↗
1/** @Environment.Discovery.Module — which environment files a load reads */2module PuduLangEnvironment.Discovery34import Std.Fs as Fs5import Std.Io as Io6import Std.List as List7import Std.Option as Option8import Std.Path as Path9import Std.Result as Result10import PuduLangEnvironment.Constants.Names as Names11import PuduLangEnvironment.Domain.Cascade as Cascade12import PuduLangEnvironment as Environment13import PuduLangEnvironment.Options as Options1415/** @Environment.Discovery.Found — the files to read and what was skipped */16export type Found = { paths: Array[Str], skipped: Array[Environment.Problem] }1718/// The files the options name, probe for, or cascade to, in reading order. An unsafe environment19/// name is refused; finding nothing is refused when strict and recorded otherwise.20export fn discover(options: &Options.Options, process: &Map[Str, Str]) -> Result[Found, Environment.Problem] {21  if options.cascade { return cascaded(options, process) }22  match options.probe {23    case Some(levels) => {24      let start = realDirectory(options.directory)25      match probe(start, levels, &[Names.DEFAULT_FILE]) {26        case Some(directory) => Ok(Found{paths: [Path.join(directory, Names.DEFAULT_FILE)], skipped: []})27        case None => nothing(options, ancestry(start, levels).map(|directory: Str| Path.join(directory, Names.DEFAULT_FILE)))28      }29    }30    case None => Ok(Found{paths: options.files.map(|path: Str| locate(options.directory, path)), skipped: []})31  }32}3334/// The first directory, from `start` up through `levels` parents, holding any of the names.35export fn probe(start: Str, levels: Int, names: &Array[Str]) -> Option[Str] {36  for directory in ancestry(start, levels) {37    for named in names {38      if Io.exists(Path.join(directory, named)) { return Some(directory) }39    }40  }41  None42}4344/// A path located against a directory: an absolute path as it is, a relative one joined to it.45export fn locate(directory: Str, path: Str) -> Str {46  if Path.isAbsolute(path) || directory.isEmpty() || directory == "." { path } else { Path.join(directory, path) }47}4849/// The cascade's existing files, from the probed directory or the first file's directory.50fn cascaded(options: &Options.Options, process: &Map[Str, Str]) -> Result[Found, Environment.Problem] {51  let name = Cascade.chooseName(&options.environmentName, process)52  if let Some(named) = name {53    if !Cascade.isSafe(named) { return Err(Environment.UnsafeEnvironmentName(named)) }54  }55  let names = Cascade.candidates(&name)56  let directory = match options.probe {57    case Some(levels) => {58      let start = realDirectory(options.directory)59      match probe(start, levels, &names) {60        case Some(found) => found61        case None => {62          var looked: Array[Str] = []63          for each in ancestry(start, levels) { looked = looked.concat(names.map(|named: Str| Path.join(each, named))) }64          return nothing(options, looked)65        }66      }67    }68    case None => Path.directoryOf(locate(options.directory, Option.unwrapOr(List.first(&options.files), Names.DEFAULT_FILE)))69  }70  let candidates = names.map(|named: Str| locate(directory, named))71  let existing = candidates.filter(|path: Str| Io.exists(path))72  if existing.isEmpty() { nothing(options, candidates) } else { Ok(Found{paths: existing, skipped: []}) }73}7475/// No file found: refused when strict, recorded otherwise.76fn nothing(options: &Options.Options, looked: Array[Str]) -> Result[Found, Environment.Problem] {77  let problem = Environment.NotFound(looked)78  if options.strict { Err(problem) } else { Ok(Found{paths: [], skipped: [problem]}) }79}8081/// The directory and up to `levels` of its parents, nearest first.82fn ancestry(start: Str, levels: Int) -> Array[Str] {83  var found = [start]84  var current = start85  var climbed = 086  while climbed < levels {87    match Path.parentOf(current) {88      case Some(parent) => {89        found = found.push(parent)90        current = parent91        climbed = climbed + 192      }93      case None => { climbed = levels }94    }95  }96  found97}9899/// The directory's real path, or the path as given when it cannot be resolved.100fn realDirectory(directory: Str) -> Str {101  Result.unwrapOr(Fs.canonical(if directory.isEmpty() { "." } else { directory }), directory)102}103