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

Functions.pudu

Pudu218 lines9.0 KB

GitHub ↗
1/** @Log.Expressions.Functions.Library — the functions expressions can call */2module PuduLangLog.Expressions.Functions34import Std.Decimal as Decimal5import Std.List as List6import Std.Math as Math7import Std.Option as Option8import Std.Regex as Regex9import Std.Text as Text10import PuduLangLog.Domain.Properties as Properties11import PuduLangLog.Expressions.Values as Values12import PuduLangLog as Log1314/// Function names that accept undefined arguments.15const TOLERANT: Array[Str] = ["coalesce", "isdefined", "typeof", "undefined", "now"]1617/// Every function name, in lower case.18const KNOWN: Array[Str] = ["coalesce", "concat", "contains", "elementat", "endswith", "indexof", "indexofmatch", "inspect", "ismatch", "isdefined", "lastindexof", "length", "nest", "now", "replace", "rest", "round", "startswith", "substring", "tagof", "tostring", "typeof", "undefined", "utcdatetime"]1920/// Whether a name, in lower case, is a function this library knows.21export fn isKnown(name: Str) -> Bool { KNOWN.contains(name) }2223/// A function applied to evaluated arguments. Any undefined argument makes the answer undefined,24/// except for `Coalesce` and `IsDefined`; `ci` makes text comparisons ignore case. `Rest` and25/// `Inspect` of a failure are answered by the evaluator.26export fn call(name: Str, arguments: &Array[Option[Log.Value]], ignoring: Bool, now: &Log.Timestamp) -> Option[Log.Value] {27  if !TOLERANT.contains(name) && List.any(arguments, |argument: Option[Log.Value]| argument == None) { return None }28  let first = Option.flatten(List.first(arguments))29  let second = Option.flatten(List.get(arguments, 1))30  match name {31    case "coalesce" => {32      for argument in arguments {33        if argument != None && argument != Some(Log.Scalar(Log.Null)) { return argument }34      }35      None36    }37    case "isdefined" => Values.boolean(first != None)38    case "undefined" => None39    case "now" => Some(Log.Scalar(Log.Moment(*now)))40    case "concat" => {41      var pieces: Array[Str] = []42      for argument in arguments { pieces = pieces.push(Values.textOf(&argument) ?) }43      Values.text(pieces.join(""))44    }45    case "contains" => Values.boolean(cased(&first, ignoring) ?.contains(cased(&second, ignoring) ?))46    case "startswith" => Values.boolean(cased(&first, ignoring) ?.startsWith(cased(&second, ignoring) ?))47    case "endswith" => Values.boolean(cased(&first, ignoring) ?.endsWith(cased(&second, ignoring) ?))48    case "indexof" => Some(Log.Scalar(Log.Integer(cased(&first, ignoring) ?.indexOf(cased(&second, ignoring) ?))))49    case "lastindexof" => Some(Log.Scalar(Log.Integer(lastIndexOf(cased(&first, ignoring) ?, cased(&second, ignoring) ?))))50    case "replace" => {51      let subject = Values.textOf(&first) ?52      let pattern = Values.textOf(&second) ?53      let replacement = Values.textOf(&Option.flatten(List.get(arguments, 2))) ?54      Values.text(if ignoring { replaceIgnoringCase(subject, pattern, replacement) } else { subject.replace(pattern, replacement) })55    }56    case "ismatch" => Values.boolean(matchAt(&first, &second, ignoring) ? >= 0)57    case "indexofmatch" => Some(Log.Scalar(Log.Integer(matchAt(&first, &second, ignoring) ?)))58    case "length" => lengthOf(&first)59    case "substring" => substring(&first, &second, &Option.flatten(List.get(arguments, 2)), arguments.length())60    case "elementat" => elementAt(&first ?, &second ?, ignoring)61    case "tagof" => match first {62      case Some(Log.Structure(shape)) => Values.text(shape.tag ?)63      case _ => None64    }65    case "typeof" => Values.text(typeOf(&first))66    case "tostring" => Values.text(Values.render(&first ?, Values.textOf(&second)))67    case "round" => {68      let places = Decimal.toInt(Values.decimalOf(&second) ?) ?69      Values.number(Decimal.roundHalfEven(Values.decimalOf(&first) ?, places))70    }71    case "utcdatetime" => match first {72      case Some(Log.Scalar(Log.Moment(moment))) => Some(Log.Scalar(Log.Moment(Log.Timestamp{..moment, offset: 0})))73      case _ => None74    }75    case "nest" => match first {76      case Some(Log.Structure(shape)) => Some(nest(&shape.properties))77      case _ => None78    }79    case "inspect" => first80    case _ => None81  }82}8384/// Text in lower case when ignoring case.85fn cased(held: &Option[Log.Value], ignoring: Bool) -> Option[Str] {86  let text = Values.textOf(held) ?87  Some(if ignoring { text.toLower() } else { text })88}8990/// The last position of a needle, or -1.91fn lastIndexOf(subject: Str, needle: Str) -> Int {92  let (before, after) = Text.breakOnEnd(subject, needle)93  if after.length() == subject.length() && !subject.endsWith(needle) { -1 } else { before.length() - needle.length() }94}9596/// Every occurrence of a pattern replaced, ignoring case.97fn replaceIgnoringCase(subject: Str, pattern: Str, replacement: Str) -> Str {98  if pattern.isEmpty() { return subject }99  var pieces: Array[Str] = []100  var rest = subject101  loop {102    let at = rest.toLower().indexOf(pattern.toLower())103    if at < 0 { return pieces.push(rest).join("") }104    pieces = pieces.push(rest.take(at)).push(replacement)105    rest = rest.drop(at + pattern.length())106  }107}108109/// The position of a regular expression's first match, -1 without one, or undefined when the110/// pattern does not compile.111fn matchAt(subject: &Option[Log.Value], pattern: &Option[Log.Value], ignoring: Bool) -> Option[Int] {112  let text = cased(subject, ignoring) ?113  let expression = Values.textOf(pattern) ?114  let compiled = match Regex.compile(if ignoring { lowerPattern(expression) } else { expression }) {115    case Ok(regex) => regex116    case Err(_) => { return None }117  }118  match Regex.find(&compiled, text) {119    case Some(found) => Some(found.start)120    case None => Some(-1)121  }122}123124/// A pattern with its literal letters lowered and its escapes kept.125fn lowerPattern(pattern: Str) -> Str {126  var pieces: Array[Str] = []127  var escaped = false128  for character in pattern.chars() {129    pieces = pieces.push(if escaped { character.toText() } else { character.toText().toLower() })130    escaped = !escaped && character == '\\'131  }132  pieces.join("")133}134135/// The length of text or of a sequence.136fn lengthOf(held: &Option[Log.Value]) -> Option[Log.Value] {137  match held {138    case Some(Log.Scalar(Log.Text(text))) => Some(Log.Scalar(Log.Integer(text.length())))139    case Some(Log.Sequence(items)) => Some(Log.Scalar(Log.Integer(items.length())))140    case _ => None141  }142}143144/// Text from a start to the end, or of a length when one is given.145fn substring(subject: &Option[Log.Value], start: &Option[Log.Value], length: &Option[Log.Value], count: Int) -> Option[Log.Value] {146  let text = Values.textOf(subject) ?147  let from = Decimal.toInt(Values.decimalOf(start) ?) ?148  if from < 0 || from > text.length() { return None }149  if count < 3 { return Values.text(text.drop(from)) }150  let size = Decimal.toInt(Values.decimalOf(length) ?) ?151  if size < 0 { return None }152  Values.text(text.slice(from, Math.min(text.length(), from + size)))153}154155/// An element of a sequence by position, or a member of a structure or dictionary by name.156export fn elementAt(target: &Log.Value, key: &Log.Value, ignoring: Bool) -> Option[Log.Value] {157  match (*target, *key) {158    case (Log.Sequence(items), _) => {159      let position = Decimal.toInt(Values.decimalOf(&Some(*key)) ?) ?160      List.get(&items, position)161    }162    case (Log.Structure(shape), Log.Scalar(Log.Text(name))) => {163      let found = List.find(&shape.properties, |member: Log.Property| if ignoring { member.name.toLower() == name.toLower() } else { member.name == name }) ?164      Some(found.value)165    }166    case (Log.Dictionary(entries), _) => {167      let found = List.find(&entries, |entry: Log.Entry| Values.equal(&Log.Scalar(entry.key), key, ignoring)) ?168      Some(found.value)169    }170    case _ => None171  }172}173174/// The kind of a value as text.175fn typeOf(held: &Option[Log.Value]) -> Str {176  match held {177    case Some(Log.Scalar(atom)) => scalarKind(&atom)178    case Some(Log.Sequence(_)) => "array"179    case Some(Log.Structure(_)) => "object"180    case Some(Log.Dictionary(_)) => "dictionary"181    case None => "undefined"182  }183}184185/// The kind of a scalar as text.186fn scalarKind(atom: &Log.Scalar) -> Str {187  match atom {188    case Log.Null => "null"189    case Log.Boolean(_) => "Bool"190    case Log.Integer(_) => "Int"191    case Log.Real(_) => "Float64"192    case Log.Exact(_) => "Decimal"193    case Log.Text(_) => "Str"194    case Log.Moment(_) => "Timestamp"195    case Log.Span(_) => "Duration"196    case Log.Binary(_) => "Bytes"197  }198}199200/// Properties with dotted names turned into nested structures: `a.b` becomes `b` inside `a`.201fn nest(properties: &Array[Log.Property]) -> Log.Value {202  var nested: Array[Log.Property] = []203  for property in properties {204    let (head, tail) = Text.breakOn(property.name, ".")205    if tail.isEmpty() {206      nested = Properties.put(&nested, head, property.value)207    } else {208      let inner = match Properties.find(&nested, head) {209        case Some(Log.Structure(shape)) => shape.properties210        case _ => []211      }212      let merged = nest(&inner.push(Log.Property{name: tail.drop(1), value: property.value}))213      nested = Properties.put(&nested, head, merged)214    }215  }216  Log.Structure(Log.Structure{tag: None, properties: nested})217}218