Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-log

Structured event logging for Pudu: message templates, enrichment, filtering, formatting, and sinks

0.1.0Apache-2.01

InstallClose

Template.pudu

Pudu247 lines10.0 KB

GitHub ↗
1/** @Log.Expressions.Template.Engine — output templates of expressions and directives */2module PuduLangLog.Expressions.Template34import Std.List as List5import PuduLangLog.Domain.Display as Display6import PuduLangLog.Domain.Json as Json7import PuduLangLog.Domain.Levels as Levels8import PuduLangLog.Expressions.Evaluator as Evaluator9import PuduLangLog.Expressions.Lexer as Lexer10import PuduLangLog.Expressions.Parser as Parser11import PuduLangLog.Expressions.Syntax as Syntax12import PuduLangLog.Expressions.Values as Values13import PuduLangLog as Log1415/** @Log.Expressions.Template.Node — literal text, a hole, or a directive block */16export type Node = TextNode(Str)17  | Hole(Syntax.Expression, Option[Str])18  | Choice(Array[Branch], Array[Node])19  | Each(Str, Option[Str], Syntax.Expression, Array[Node], Array[Node], Array[Node])2021/** @Log.Expressions.Template.Branch — a condition and the nodes it guards */22export type Branch = { condition: Syntax.Expression, body: Array[Node] }2324/** @Log.Expressions.Template.Piece — a scanned part before blocks are nested */25type Piece = Literal(Str) | Placeholder(Str) | Directive(Str)2627/// The template's nodes, or why it could not be read.28export fn parse(text: Str) -> Result[Array[Node], Str] {29  let pieces = scan(text) ?30  let (nodes, next, closer) = block(&pieces, 0) ?31  if closer != "" { return Err("unexpected `\{#" + closer + "\}`") }32  if next < pieces.length() { return Err("unexpected directive") }33  Ok(nodes)34}3536/// Every expression in the nodes, in order.37export fn expressionsIn(nodes: &Array[Node]) -> Array[Syntax.Expression] {38  var found: Array[Syntax.Expression] = []39  for node in nodes {40    found = found.concat(match node {41        case TextNode(_) => []42        case Hole(expression, _) => [expression]43        case Choice(branches, otherwise) => {44          var inner: Array[Syntax.Expression] = []45          for branch in branches { inner = inner.push(branch.condition).concat(expressionsIn(&branch.body)) }46          inner.concat(expressionsIn(&otherwise))47        }48        case Each(_, _, source, body, delimiter, empty) => [source].concat(expressionsIn(&body)).concat(expressionsIn(&delimiter)).concat(expressionsIn(&empty))49      })50  }51  found52}5354/// The text split into literals, holes, and directives; doubled braces are literal braces.55fn scan(text: Str) -> Result[Array[Piece], Str] {56  let characters = text.chars()57  var pieces: Array[Piece] = []58  var literal: Array[Str] = []59  var index = 060  while index < characters.length() {61    let character = characters[index]62    let doubled = List.get(&characters, index + 1) == Some(character)63    if (character == '\{' || character == '\}') && doubled {64      literal = literal.push(character.toText())65      index = index + 266    } else if character == '\{' {67      if !literal.isEmpty() { pieces = pieces.push(Literal(literal.join(""))) }68      literal = []69      let close = closing(&characters, index + 1) ?70      let content = text.slice(index + 1, close).trim()71      pieces = pieces.push(if content.startsWith("#") { Directive(content.drop(1).trim()) } else { Placeholder(content) })72      index = close + 173    } else {74      literal = literal.push(character.toText())75      index = index + 176    }77  }78  if !literal.isEmpty() { pieces = pieces.push(Literal(literal.join(""))) }79  Ok(pieces)80}8182/// The index of the brace closing a hole, skipping nested braces, brackets, and quoted text.83fn closing(characters: &Array[Char], start: Int) -> Result[Int, Str] {84  var depth = 085  var quoted = false86  var index = start87  while index < characters.length() {88    let character = characters[index]89    if quoted {90      if character == '\'' { quoted = false }91    } else if character == '\'' {92      quoted = true93    } else if character == '\{' || character == '[' {94      depth = depth + 195    } else if character == ']' {96      depth = depth - 197    } else if character == '\}' {98      if depth == 0 { return Ok(index) }99      depth = depth - 1100    }101    index = index + 1102  }103  Err("a hole opened at " + show(start - 1) + " is not closed")104}105106/// Nodes until the end or a directive that closes the enclosing block, the index after them, and107/// that directive's text.108fn block(pieces: &Array[Piece], start: Int) -> Result[(Array[Node], Int, Str), Str] {109  var nodes: Array[Node] = []110  var index = start111  while index < pieces.length() {112    match pieces[index] {113      case Literal(text) => {114        nodes = nodes.push(TextNode(text))115        index = index + 1116      }117      case Placeholder(content) => {118        nodes = nodes.push(holeOf(content) ?)119        index = index + 1120      }121      case Directive(content) => {122        if content == "end" || content == "else" || content == "delimit" || content.startsWith("else if ") { return Ok((nodes, index + 1, content)) }123        let (node, next) = if content.startsWith("if ") { choiceAt(pieces, index + 1, content.drop(3)) ? } else if content.startsWith("each ") { eachAt(pieces, index + 1, content.drop(5)) ? } else { return Err("unknown directive `\{#" + content + "\}`") }124        nodes = nodes.push(node)125        index = next126      }127    }128  }129  Ok((nodes, index, ""))130}131132/// A hole: an expression and an optional format after its first top-level `:`.133fn holeOf(content: Str) -> Result[Node, Str] {134  let found = Lexer.tokens(content) ?135  let (expression, next) = Parser.expressionAt(&found, 0) ?136  let token = found[next]137  if token.kind == Lexer.End { return Ok(Hole(expression, None)) }138  if token.kind == Lexer.Symbol && token.text == ":" { return Ok(Hole(expression, Some(content.drop(token.position + 1)))) }139  Err("unexpected `" + token.text + "` in hole `" + content + "`")140}141142/// An `#if` block with its `#else if` and `#else` parts, and the index after its `#end`.143fn choiceAt(pieces: &Array[Piece], start: Int, condition: Str) -> Result[(Node, Int), Str] {144  var branches: Array[Branch] = []145  var current = Parser.parse(condition) ?146  var index = start147  loop {148    let (body, next, closer) = block(pieces, index) ?149    branches = branches.push(Branch{condition: current, body: body})150    if closer == "end" { return Ok((Choice(branches, []), next)) }151    if closer == "else" {152      let (otherwise, after, final) = block(pieces, next) ?153      if final != "end" { return Err("`\{#else\}` must be followed by `\{#end\}`") }154      return Ok((Choice(branches, otherwise), after))155    }156    if !closer.startsWith("else if ") { return Err("`\{#if\}` is not closed") }157    current = Parser.parse(closer.drop(8)) ?158    index = next159  }160}161162/// An `#each` block with its optional `#delimit` and `#else` parts, and the index after its `#end`.163fn eachAt(pieces: &Array[Piece], start: Int, header: Str) -> Result[(Node, Int), Str] {164  let parts = header.split(" in ")165  if parts.length() < 2 { return Err("`\{#each\}` needs `in`") }166  let names = parts[0].split(",").map(|name: Str| name.trim())167  let source = Parser.parse(parts.slice(1, parts.length()).join(" in ")) ?168  let second = List.get(&names, 1)169  let (body, afterBody, closer) = block(pieces, start) ?170  var delimiter: Array[Node] = []171  var empty: Array[Node] = []172  var next = afterBody173  var last = closer174  if last == "delimit" {175    let (inner, after, final) = block(pieces, next) ?176    delimiter = inner177    next = after178    last = final179  }180  if last == "else" {181    let (inner, after, final) = block(pieces, next) ?182    empty = inner183    next = after184    last = final185  }186  if last != "end" { return Err("`\{#each\}` is not closed") }187  Ok((Each(names[0], second, source, body, delimiter, empty), next))188}189190/// The nodes rendered for an event.191export fn render(nodes: &Array[Node], within: &Evaluator.Scope) -> Str {192  var out: Array[Str] = []193  for node in nodes {194    out = out.push(match node {195        case TextNode(text) => text196        case Hole(expression, format) => holeText(&expression, &format, within)197        case Choice(branches, otherwise) => {198          match List.find(&branches, |branch: Branch| Values.isTrue(&Evaluator.evaluate(&branch.condition, within))) {199            case Some(branch) => render(&branch.body, within)200            case None => render(&otherwise, within)201          }202        }203        case Each(name, second, source, body, delimiter, empty) => eachText(name, &second, &source, &body, &delimiter, &empty, within)204      })205  }206  out.join("")207}208209/// A hole's text: nothing when undefined, levels and messages by their own rules, text as it is,210/// scalars under the format, and anything else as JSON.211fn holeText(expression: &Syntax.Expression, format: &Option[Str], within: &Evaluator.Scope) -> Str {212  if *expression == Syntax.Name("@l") { return Levels.moniker(within.event.level, format) }213  if *expression == Syntax.Name("@m") { return Display.plain(&Display.message(&within.event.template, &within.event.properties, &Some("lj"))) }214  match Evaluator.evaluate(expression, within) {215    case None => ""216    case Some(Log.Scalar(atom)) => Values.render(&Log.Scalar(atom), *format)217    case Some(held) => Json.value(&held, &Some(Display.TYPE_TAG))218  }219}220221/// An `#each` block rendered once per element or member, delimited, or its `#else` part when there222/// is none.223fn eachText(name: Str, second: &Option[Str], source: &Syntax.Expression, body: &Array[Node], delimiter: &Array[Node], empty: &Array[Node], within: &Evaluator.Scope) -> Str {224  let bindings: Array[(Log.Value, Log.Value)] = match Evaluator.evaluate(source, within) {225    case Some(Log.Sequence(items)) => {226      var pairs: Array[(Log.Value, Log.Value)] = []227      var position = 0228      for item in items {229        pairs = pairs.push((item, Log.Scalar(Log.Integer(position))))230        position = position + 1231      }232      pairs233    }234    case Some(Log.Structure(shape)) => shape.properties.map(|member: Log.Property| (Log.Scalar(Log.Text(member.name)), member.value))235    case _ => []236  }237  if bindings.isEmpty() { return render(empty, within) }238  var parts: Array[Str] = []239  for binding in bindings {240    var bound = [(name, binding[0])]241    if let Some(other) = *second { bound = bound.push((other, binding[1])) }242    let inner = Evaluator.Scope{..*within, locals: bound.concat(within.locals)}243    parts = parts.push(render(body, &inner))244  }245  parts.join(render(delimiter, within))246}247