
Capture.pudu
Pudu203 lines8.9 KB
1/** @Log.Domain.Capture.Module — arguments bound to holes under capture policies */2module PuduLangLog.Domain.Capture34import Std.List as List5import Std.Math as Math6import Std.Option as Option7import PuduLangLog.Domain.Display as Display8import PuduLangLog.Domain.Json as Json9import PuduLangLog.Domain.Properties as Properties10import PuduLangLog as Log1112/** @Log.Domain.Capture.Policy — limits and rules applied while capturing */13export type Policy = {14 maximumDepth: Int,15 maximumStringLength: Int,16 maximumCollectionCount: Int,17 scalars: Array[Str],18 transforms: Array[Transform],19 custom: Array[fn(&Log.Value) -> Option[Log.Value]]20}2122/** @Log.Domain.Capture.Transform — a replacement for structures of one tag */23export type Transform = { tag: Str, apply: fn(Log.Value) -> Log.Value }2425/** @Log.Domain.Capture.Bound — the properties bound and any problems met */26export type Bound = { properties: Array[Log.Property], problems: Array[Str] }272829export const UNLIMITED: Int = 2147483647303132const LONGEST_BYTES: Int = 1024333435const KEPT_BYTES: Int = 16363738const DEPTH_REACHED: Str = "Maximum destructuring depth reached."394041export fn defaults() -> Policy {42 Policy{maximumDepth: 10, maximumStringLength: UNLIMITED, maximumCollectionCount: UNLIMITED, scalars: [], transforms: [], custom: []}43}4445464748export fn capture(policy: &Policy, held: &Log.Value, hint: Log.Hint) -> Log.Value {49 let (value, _cut) = captured(policy, held, hint, 1)50 value51}525354fn captured(policy: &Policy, held: &Log.Value, hint: Log.Hint, depth: Int) -> (Log.Value, Bool) {55 if depth > policy.maximumDepth { return (Log.Scalar(Log.Null), true) }56 if hint == Log.Stringify { return (Log.Scalar(Log.Text(truncated(policy, stringified(held)))), false) }57 match held {58 case Log.Scalar(atom) => (Log.Scalar(scalarOf(policy, &atom)), false)59 case Log.Sequence(items) => {60 var members: Array[Log.Value] = []61 var cut = false62 for item in List.take(&items, policy.maximumCollectionCount) {63 let (member, deeper) = captured(policy, &item, hint, depth + 1)64 members = members.push(member)65 cut = cut || deeper66 }67 (Log.Sequence(members), cut)68 }69 case Log.Dictionary(entries) => {70 var members: Array[Log.Entry] = []71 var cut = false72 for entry in List.take(&entries, policy.maximumCollectionCount) {73 let (member, deeper) = captured(policy, &entry.value, hint, depth + 1)74 members = members.push(Log.Entry{key: scalarOf(policy, &entry.key), value: member})75 cut = cut || deeper76 }77 (Log.Dictionary(members), cut)78 }79 case Log.Structure(shape) => structureOf(policy, held, &shape, hint, depth)80 }81}8283848586fn structureOf(policy: &Policy, held: &Log.Value, shape: &Log.Structure, hint: Log.Hint, depth: Int) -> (Log.Value, Bool) {87 let scalarTag = match shape.tag {88 case Some(tag) => policy.scalars.contains(tag)89 case None => false90 }91 if hint != Log.Destructure || scalarTag { return (Log.Scalar(Log.Text(truncated(policy, stringified(held)))), false) }92 match replacementOf(policy, held, shape) {93 case Some(Log.Structure(replaced)) => membersOf(policy, &replaced, depth)94 case Some(replaced) => captured(policy, &replaced, Log.Destructure, depth)95 case None => membersOf(policy, shape, depth)96 }97}9899100fn replacementOf(policy: &Policy, held: &Log.Value, shape: &Log.Structure) -> Option[Log.Value] {101 for rule in policy.custom {102 if let Some(replaced) = rule(held) { return Some(replaced) }103 }104 let tag = shape.tag ?105 let transform = List.find(&policy.transforms, |candidate: Transform| candidate.tag == tag) ?106 Some((transform.apply)(*held))107}108109110fn membersOf(policy: &Policy, shape: &Log.Structure, depth: Int) -> (Log.Value, Bool) {111 var members: Array[Log.Property] = []112 var cut = false113 for property in shape.properties {114 let (member, deeper) = captured(policy, &property.value, Log.Destructure, depth + 1)115 members = members.push(Log.Property{name: property.name, value: member})116 cut = cut || deeper117 }118 (Log.Structure(Log.Structure{tag: shape.tag, properties: members}), cut)119}120121122fn scalarOf(policy: &Policy, atom: &Log.Scalar) -> Log.Scalar {123 match atom {124 case Log.Text(text) => Log.Text(truncated(policy, text))125 case Log.Binary(bytes) => {126 if bytes.length() <= LONGEST_BYTES { return *atom }127 Log.Text(Json.hexOf(&bytes.take(KEPT_BYTES)) + "... (" + show(bytes.length()) + " bytes)")128 }129 case _ => *atom130 }131}132133134fn stringified(held: &Log.Value) -> Str {135 match held {136 case Log.Scalar(Log.Text(text)) => text137 case _ => Display.plain(&Display.value(held, &Some("l")))138 }139}140141142fn truncated(policy: &Policy, text: Str) -> Str {143 if text.length() <= policy.maximumStringLength { return text }144 text.take(Math.max(0, policy.maximumStringLength - 1)) + "…"145}146147148149export fn bind(policy: &Policy, template: &Log.Template, arguments: &Array[Log.Value]) -> Bound {150 if arguments.isEmpty() {151 let problems = if template.holes.isEmpty() { [] } else { ["Required properties not provided for: " + template.text] }152 return Bound{properties: [], problems: problems}153 }154 match template.binding {155 case Log.Unbound => Bound{properties: [], problems: ["Parameters provided for message template with no properties: " + template.text]}156 case Log.Positional => positional(policy, template, arguments)157 case Log.Named => named(policy, template, arguments)158 }159}160161162fn positional(policy: &Policy, template: &Log.Template, arguments: &Array[Log.Value]) -> Bound {163 var slots: Array[Option[Log.Property]] = arguments.map(|_argument: Log.Value| None)164 var problems: Array[Str] = []165 for hole in template.holes {166 let position = Option.unwrapOr(hole.position, 0)167 match List.get(arguments, position) {168 case Some(argument) => {169 let (held, cut) = captured(policy, &argument, hole.hint, 1)170 if cut { problems = problems.push(DEPTH_REACHED) }171 slots = List.take(&slots, position).push(Some(Log.Property{name: hole.name, value: held})).concat(List.drop(&slots, position + 1))172 }173 case None => { problems = problems.push("Unassigned positional value " + show(position) + " in: " + template.text) }174 }175 }176 var properties: Array[Log.Property] = []177 for slot in slots {178 if let Some(property) = slot { properties = properties.push(property) }179 }180 if properties.length() != arguments.length() { problems = problems.push("Positional property count does not match parameter count: " + template.text) }181 Bound{properties: properties, problems: problems}182}183184185fn named(policy: &Policy, template: &Log.Template, arguments: &Array[Log.Value]) -> Bound {186 var problems: Array[Str] = []187 if List.any(&template.holes, |hole: Log.Hole| hole.position != None) { problems = problems.push("Message template is malformed: " + template.text) }188 if template.holes.length() != arguments.length() { problems = problems.push("Named property count does not match parameter count: " + template.text) }189 var properties: Array[Log.Property] = []190 var index = 0191 for argument in arguments {192 let (name, hint) = match List.get(&template.holes, index) {193 case Some(hole) => (hole.name, hole.hint)194 case None => ("__" + show(index), Log.Default)195 }196 let (held, cut) = captured(policy, &argument, hint, 1)197 if cut { problems = problems.push(DEPTH_REACHED) }198 properties = Properties.put(&properties, name, held)199 index = index + 1200 }201 Bound{properties: properties, problems: problems}202}203