
Functions.pudu
Pudu218 lines9.0 KB
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 Log131415const TOLERANT: Array[Str] = ["coalesce", "isdefined", "typeof", "undefined", "now"]161718const 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"]192021export fn isKnown(name: Str) -> Bool { KNOWN.contains(name) }2223242526export 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}838485fn cased(held: &Option[Log.Value], ignoring: Bool) -> Option[Str] {86 let text = Values.textOf(held) ?87 Some(if ignoring { text.toLower() } else { text })88}899091fn 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}959697fn 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}108109110111fn 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}123124125fn 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}134135136fn 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}143144145fn 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}154155156export 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}173174175fn 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}184185186fn 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}199200201fn 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