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

Loader.pudu

Pudu105 lines4.5 KB

GitHub ↗
1/** @Environment.Loader.Module — reads, parses, and merges environment sources */2module PuduLangEnvironment.Loader34import Std.Env as Env5import Std.Map as Map6import PuduLangEnvironment.Discovery as Discovery7import PuduLangEnvironment.Domain.Layers as Layers8import PuduLangEnvironment.Domain.Parser as Parser9import PuduLangEnvironment as Environment10import PuduLangEnvironment.Options as Options11import PuduLangEnvironment.Source as Source12import PuduLangEnvironment.Variables as Variables1314/** @Environment.Loader.Collected — merged values, what was read, what was skipped */15type Collected = { values: Map[Str, Str], sources: Array[Str], skipped: Array[Environment.Problem] }1617/// The merged values of the sources the options name, over the process environment as it is.18export fn read(options: &Options.Options) -> Result[Map[Str, Str], Environment.Problem] {19  readOver(options, &Map.fromPairs(&Env.variables()))20}2122/// The merged values settled over the process environment as it is.23export fn load(options: &Options.Options) -> Result[Variables.Variables, Environment.Problem] {24  loadOver(options, &Map.fromPairs(&Env.variables()))25}2627/// The merged values of the sources the options name, expanding over `process` instead of the28/// process environment.29export fn readOver(options: &Options.Options, process: &Map[Str, Str]) -> Result[Map[Str, Str], Environment.Problem] {30  Ok(collect(options, process) ?.values)31}3233/// The merged values settled over `process` instead of the process environment.34export fn loadOver(options: &Options.Options, process: &Map[Str, Str]) -> Result[Variables.Variables, Environment.Problem] {35  let collected = collect(options, process) ?36  Ok(Variables.settled(&collected.values, process, options.overwrite, collected.sources, collected.skipped))37}3839/** @Environment.Loader.Loading — a fluent chain ending in a read or load */40export trait Loading {41  fn read(self: &Self) -> Result[Map[Str, Str], Environment.Problem]42  fn load(self: &Self) -> Result[Variables.Variables, Environment.Problem]43}4445impl Loading for Options.Options {46  /// The merged values these options name.47  fn read(self: &Self) -> Result[Map[Str, Str], Environment.Problem] { read(self) }4849  /// The merged values these options name, settled over the process environment.50  fn load(self: &Self) -> Result[Variables.Variables, Environment.Problem] { load(self) }51}5253/// Every source read and parsed in order, threading known assignments, and merged. Without54/// overwriting, the process values a load keeps are known from the start, so expansions see what55/// the program will see.56fn collect(options: &Options.Options, process: &Map[Str, Str]) -> Result[Collected, Environment.Problem] {57  let problems = Options.validate(options)58  if !problems.isEmpty() { return Err(Environment.InvalidOptions(problems)) }59  var skipped: Array[Environment.Problem] = []60  var sources: Array[Source.Source] = []61  if options.sources.isEmpty() {62    let found = Discovery.discover(options, process) ?63    skipped = found.skipped64    sources = found.paths.map(|path: Str| Source.File(path))65  } else {66    sources = options.sources.map(|given: Source.Source| located(options.directory, given))67  }68  let rules = Parser.Rules {69    trimValues: options.trimValues,70    exportSyntax: options.exportSyntax,71    inlineComments: options.inlineComments,72    expansion: options.expansion,73    overwrite: options.overwrite74  }75  var known: Map[Str, Str] = if options.overwrite { Map.empty() } else { Map.filterValues(process, |held: Str| !held.isEmpty()) }76  var layers: Array[Array[Layers.Entry]] = []77  var opened: Array[Str] = []78  for source in sources {79    let named = Source.label(&source)80    match Source.lines(&source, options.encoding) {81      case Ok(lines) => match Parser.parse(&lines, &rules, known, process) {82        case Ok(parsed) => {83          known = parsed.known84          layers = layers.push(parsed.entries)85          opened = opened.push(named)86        }87        case Err(open) => { return Err(Environment.UnclosedQuote(named, open.line, open.key)) }88      }89      case Err(problem) => {90        if options.strict || !Environment.isSkippable(&problem) { return Err(problem) }91        skipped = skipped.push(problem)92      }93    }94  }95  Ok(Collected{values: Layers.merge(&layers, options.overwrite), sources: opened, skipped: skipped})96}9798/// A file source located against the options' directory; other sources as they are.99fn located(directory: Str, given: Source.Source) -> Source.Source {100  match given {101    case Source.File(path) => Source.File(Discovery.locate(directory, path))102    case _ => given103  }104}105