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

Binding.pudu

Pudu148 lines6.1 KB

GitHub ↗
1/** @Environment.Binding.Module — records read from variables by derivation */2module PuduLangEnvironment.Binding34import Std.App.Secret as Secret5import Std.Meta as Meta6import PuduLangEnvironment.Constants.Names as Names7import PuduLangEnvironment.Domain.Keys as Keys8import PuduLangEnvironment.Domain.Values as Values9import PuduLangEnvironment as Environment10import PuduLangEnvironment.Variables as Variables1112/** @Environment.Binding.Variable — a type one variable's text spells */13export trait Variable {14  /// The value the text of `key` spells; `None` text means the variable is unset or empty.15  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem]16  /// Whether an unset variable is a value rather than a missing one.17  fn optional() -> Bool18  /// The value rendered for a reader; a secret renders as a placeholder.19  fn rendered(self: &Self) -> Str20}2122impl Variable for Str {23  /// The text itself.24  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem] { converted(key, text, |held: Str| Some(held), "") }25  /// A text variable is required.26  fn optional() -> Bool { false }27  /// The text in quotes.28  fn rendered(self: &Self) -> Str { show(*self) }29}3031impl Variable for Int {32  /// The whole number the text spells.33  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem] { converted(key, text, Values.toInt, Names.KIND_INT) }34  /// A whole-number variable is required.35  fn optional() -> Bool { false }36  /// The number as written.37  fn rendered(self: &Self) -> Str { show(*self) }38}3940impl Variable for Float64 {41  /// The floating-point number the text spells.42  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem] { converted(key, text, Values.toFloat, Names.KIND_FLOAT) }43  /// A float variable is required.44  fn optional() -> Bool { false }45  /// The number as written.46  fn rendered(self: &Self) -> Str { show(*self) }47}4849impl Variable for Decimal {50  /// The exact decimal number the text spells.51  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem] { converted(key, text, Values.toDecimal, Names.KIND_DECIMAL) }52  /// A decimal variable is required.53  fn optional() -> Bool { false }54  /// The number as written.55  fn rendered(self: &Self) -> Str { show(*self) }56}5758impl Variable for Bool {59  /// The truth value the text spells.60  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem] { converted(key, text, Values.toBool, Names.KIND_BOOL) }61  /// A truth-value variable is required.62  fn optional() -> Bool { false }63  /// `true` or `false`.64  fn rendered(self: &Self) -> Str { show(*self) }65}6667impl Variable for Secret.Secret {68  /// The text wrapped so that it renders as a placeholder.69  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem] { converted(key, text, |held: Str| Some(Secret.secret(held)), "") }70  /// A secret variable is required.71  fn optional() -> Bool { false }72  /// The placeholder, never the value.73  fn rendered(self: &Self) -> Str { Names.REDACTED }74}7576impl [A: Variable] Variable for Option[A] {77  /// Nothing when unset, else what the text spells as `A`.78  fn fromVariable(key: Str, text: Option[Str]) -> Result[Self, Environment.Problem] {79    match text {80      case Some(_) => Ok(Some(A.fromVariable(key, text) ?))81      case None => Ok(None)82    }83  }84  /// An optional variable may be unset.85  fn optional() -> Bool { true }86  /// `None`, or the held value rendered as `A` renders it.87  fn rendered(self: &Self) -> Str {88    match self {89      case Some(held) => "Some(" + held.rendered() + ")"90      case None => "None"91    }92  }93}9495/** @Environment.Binding.Bind — a record read from environment variables */96export trait Bind {97  /// The record whose every field is read from its variable, or why it cannot be.98  fn bind(variables: &Variables.Variables) -> Result[Self, Environment.Problem]99}100101/// A record reads each field from the variable `@env("NAME")` names, else from its name in upper102/// snake case; an unset variable takes `@default("text")` when the field has one. Every required103/// variable that is unset is reported at once as `Missing`; then the first value that does not104/// convert is reported as `Malformed`.105export derive Bind for T: Record {106  fn bind(variables: &Variables.Variables) -> Result[T, Environment.Problem] {107    let missing = Meta.collect[T](fn(field: Meta.Field[T, F]) -> Option[Str] where F: Variable {108        let key = field.attributeOr("env", Keys.fromField(field.name))109        if F.optional() || field.has("default") || Variables.has(variables, key) { None } else { Some(key) }110      })111    if !missing.isEmpty() { return Err(Environment.Missing(missing)) }112    Meta.build[T](fn(field: Meta.Field[T, F]) -> Result[F, Environment.Problem] where F: Variable {113        let key = field.attributeOr("env", Keys.fromField(field.name))114        let found = Variables.find(variables, key)115        let text = if found == None && field.has("default") { Some(field.attributeOr("default", "")) } else { found }116        F.fromVariable(key, text)117      })118  }119}120121/** @Environment.Binding.Redacted — a record rendered with its secrets hidden */122export trait Redacted {123  /// The record rendered as its name and fields, each `@secret` field as a placeholder.124  fn redacted(self: &Self) -> Str125}126127/// A record renders like `show` does, except that every secret, and every field marked128/// `@secret`, renders as `[REDACTED]`.129export derive Redacted for T: Record {130  fn redacted(self: &T) -> Str {131    let parts = Meta.collect[T](fn(field: Meta.Field[T, F]) -> Option[Str] where F: Variable {132        Some(field.name + ": " + if field.has("secret") { Names.REDACTED } else { field.get(self).rendered() })133      })134    Meta.nameOf[T]() + "\{" + parts.join(", ") + "\}"135  }136}137138/// The value the text of `key` converts to, `Missing` when unset, or `Malformed` naming the kind.139fn converted[T](key: Str, text: Option[Str], convert: fn(Str) -> Option[T], kind: Str) -> Result[T, Environment.Problem] {140  match text {141    case Some(held) => match convert(held) {142      case Some(value) => Ok(value)143      case None => Err(Environment.Malformed(key, kind))144    }145    case None => Err(Environment.Missing([key]))146  }147}148