src/Result.ps1
|
function Add-QuietTaskResult { param($Result) foreach($task in $script:QuietTasks){if($task.Seen.Add($Result.Internal.Id)){$task.Results.Add($Result)}} } function New-QuietResult { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions','',Justification='Constructs an in-memory value; no external state is changed.')] [CmdletBinding()]param([string]$Summary='Command completed.', [ValidateSet('Success','Failed','Timeout','Cancelled','Unsupported')][string]$Outcome='Success', [AllowNull()]$Data=$null,[Collections.IDictionary]$Execution=@{},[AllowNull()]$ErrorInfo=$null, [string[]]$Reasons=@(),[scriptblock]$Projection,[int]$MaxOutputBytes=4096,[switch]$NoRegister) $result=[pscustomobject][ordered]@{ SchemaVersion=2;Summary=$Summary;Outcome=$Outcome;Data=$Data;Execution=[pscustomobject]$Execution;Error=$ErrorInfo Completeness=[pscustomobject]@{Source=($Outcome -eq 'Success');Analysis=($Outcome -eq 'Success');Capture=$true} Truncation=[pscustomobject]@{Applied=($Reasons.Count -gt 0);Reasons=@($Reasons);Omitted=0L} Measurement=[pscustomobject]@{Status='NotDelivered';Profile=$null;Version=$null} Capture=$null;Projection=$Projection;RecordId=$null Internal=@{PresentationMs=0.0;RecallMs=0.0;Id=[guid]::NewGuid().ToString('N');Sources=[ordered]@{};DeliveryPaths=[Collections.Generic.HashSet[string]]::new();Origin=$null;Records=@();PresentationBudget=$(if($PSBoundParameters.ContainsKey('MaxOutputBytes')){$MaxOutputBytes}else{$null})} } $result.PSObject.TypeNames.Insert(0,'QuietCli.Result') if(!$NoRegister -and !$script:QuietBuildingContract){Add-QuietTaskResult $result} $result } function Invoke-QuietStep { [CmdletBinding()]param([Parameter(Mandatory)][string]$Name,[Parameter(Mandatory)][scriptblock]$ScriptBlock) if(!$script:QuietTasks.Count){throw 'Invoke-QuietStep requires Invoke-QuietTask.'} try{ $items=@(Invoke-QuietCallback $ScriptBlock) if($items.Count -ne 1 -or $items[0].PSObject.TypeNames -notcontains 'QuietCli.Result'){throw 'A step must return one QuietCli result.'} $result=$items[0];$result.Execution|Add-Member -NotePropertyName Step -NotePropertyValue $Name -Force Add-QuietTaskResult $result $result }catch{New-QuietResult -Summary "Step $Name failed." -Outcome Failed -ErrorInfo @{Code='StepFailed';Message=$_.Exception.Message}} } function Invoke-QuietTask { [CmdletBinding()]param([Parameter(Mandatory)][scriptblock]$ScriptBlock,[object[]]$ArgumentList=@(), [int]$MaxItems=20,[int]$MaxOutputBytes=4096,[switch]$AsJson,[switch]$AsYaml,[switch]$AsObject,[switch]$Record) if(@($AsObject,$AsJson,$AsYaml|Where-Object {$_}).Count -gt 1){throw 'AsObject, AsJson and AsYaml are exclusive.'} $watch=[Diagnostics.Stopwatch]::StartNew() $task=@{Results=[Collections.Generic.List[object]]::new();Seen=[Collections.Generic.HashSet[string]]::new();Record=[bool]$Record} $script:QuietTasks.Push($task);$failure=$null;$values=@() try{$values=@(Invoke-QuietCallback $ScriptBlock $ArgumentList|Where-Object {$_.PSObject.TypeNames -notcontains 'QuietCli.Result'})} catch{$failure=@{Code='TaskFailed';Message=$_.Exception.Message}} finally{[void]$script:QuietTasks.Pop()} $issues=@($task.Results|Where-Object Outcome -ne 'Success') $outcome=if($issues.Count){$issues[0].Outcome}elseif($failure){'Failed'}else{'Success'} $data=if($values.Count -eq 1){$values[0]}elseif($values.Count){,$values}else{$null} $result=New-QuietResult -Summary "Task: $($task.Results.Count) steps." -Outcome $outcome -Data $data -ErrorInfo $failure -NoRegister -Execution @{ Steps=$task.Results.Count;ElapsedMs=$watch.Elapsed.TotalMilliseconds;ExitCode=$null AnalysisComplete=(!$failure -and @($task.Results|Where-Object {!$_.Completeness.Analysis}).Count -eq 0) Issues=@($issues|Select-Object -First 20|ForEach-Object {@{Outcome=$_.Outcome;Error=$_.Error;ExitCode=$_.Execution.ExitCode}}) } foreach($step in $task.Results){foreach($key in $step.Internal.Sources.Keys){$result.Internal.Sources[$key]=$step.Internal.Sources[$key]}} $result.Completeness.Source=(@($task.Results|Where-Object {!$_.Completeness.Source}).Count -eq 0) $result.Completeness.Analysis=$result.Execution.AnalysisComplete $result.Completeness.Capture=(@($task.Results|Where-Object {!$_.Completeness.Capture}).Count -eq 0) if($Record){$result.Internal.Records=@($task.Results.RecordId|Where-Object {$_});Save-QuietRecord $result -Explicit|Out-Null} if($AsJson -or $AsYaml){Out-QuietResult -InputObject $result -AsYaml:$AsYaml -MaxItems $MaxItems -MaxOutputBytes $MaxOutputBytes}else{$result} } function ConvertTo-QuietYaml { param($Value,[int]$Indent=0) $pad=' '*$Indent if($null -eq $Value){return 'null'} if($Value -is [string] -or $Value -is [ValueType]){return ConvertTo-Json -InputObject $Value -Compress -Depth 64} if($Value -is [Collections.IDictionary] -or ($Value -is [pscustomobject] -and $Value -isnot [Collections.IEnumerable])){ $keys=if($Value -is [Collections.IDictionary]){@($Value.Keys)}else{@($Value.PSObject.Properties.Name)} if(!$keys.Count){return '{}'} $lines=foreach($key in $keys){ $child=$Value.$key;$name=ConvertTo-Json ([string]$key) -Compress $rendered=ConvertTo-QuietYaml $child ($Indent+2) if($rendered -notin @('{}','[]') -and ($child -is [Collections.IDictionary] -or $child -is [pscustomobject] -or ($child -is [Collections.IEnumerable] -and $child -isnot [string] -and @($child).Count))){ ('{0}{1}:' -f $pad,$name)+[Environment]::NewLine+$rendered }else{('{0}{1}: {2}' -f $pad,$name,$rendered)} } return $lines -join [Environment]::NewLine } $items=@($Value);if(!$items.Count){return '[]'} $lines=foreach($item in $items){$rendered=ConvertTo-QuietYaml $item ($Indent+2);if($rendered -in @('{}','[]') -or $item -is [ValueType] -or $item -is [string] -or $null -eq $item){"$pad- $rendered"}else{"$pad-"+[Environment]::NewLine+$rendered}} $lines -join [Environment]::NewLine } function Limit-QuietProjection { [CmdletBinding()]param([Parameter(Mandatory)]$Data,[Parameter(Mandatory)][string[]]$Property,[int]$MaxItems=20) $copy=[ordered]@{} if($Data -is [Collections.IDictionary]){foreach($key in $Data.Keys){$copy[$key]=$Data[$key]}}else{foreach($p in $Data.PSObject.Properties){$copy[$p.Name]=$p.Value}} $omitted=[ordered]@{} foreach($name in $Property){if($null -ne $copy[$name]){$list=@($copy[$name]);$copy[$name]=@($list|Select-Object -First $MaxItems);if($list.Count -gt $MaxItems){$omitted[$name]=$list.Count-$MaxItems}}} if($omitted.Count){$copy['Omitted']=[pscustomobject]$omitted} $projection=[pscustomobject]$copy;$projection.PSObject.TypeNames.Insert(0,'QuietCli.Projection');$projection } function Out-QuietResult { [CmdletBinding()]param([Parameter(Mandatory,ValueFromPipeline)]$InputObject,[switch]$AsJson,[switch]$AsYaml, [int]$MaxItems=-1,[int]$MaxOutputBytes=-1,[int]$ExcerptLines=-1,[ValidateSet('compact','full')][string]$Envelope) process{ $deliveryWatch=[Diagnostics.Stopwatch]::StartNew() $settings=Get-QuietConfiguration if($MaxItems -lt 0){$MaxItems=$settings.MaxItems};if($MaxOutputBytes -lt 0){$MaxOutputBytes=$settings.MaxOutputBytes} if($ExcerptLines -lt 0){$ExcerptLines=$settings.ExcerptLines};if(!$Envelope){$Envelope=$settings.Envelope} if($MaxOutputBytes -lt 256 -or $MaxItems -lt 0){throw 'Invalid delivery limits.'} $result=if($InputObject.PSObject.TypeNames -contains 'QuietCli.Result'){$InputObject}else{New-QuietResult -Data $InputObject -NoRegister} if(!$PSBoundParameters.ContainsKey('MaxOutputBytes') -and $result.Internal.PresentationBudget){$MaxOutputBytes=$result.Internal.PresentationBudget} $data=$result.Data;$reasons=[Collections.Generic.List[string]]::new();foreach($reason in $result.Truncation.Reasons){$reasons.Add($reason)} if($result.Projection){try{$projected=@(Invoke-QuietCallback $result.Projection @($result.Data,@{MaxItems=$MaxItems;MaxOutputBytes=$MaxOutputBytes}));$data=if($projected.Count -eq 1){$projected[0]}elseif($projected.Count){,$projected}else{$null}}catch{$data=$null;$reasons.Add('ProjectionFailed')}} $omitted=[long]$result.Truncation.Omitted if($data -and $data.PSObject.TypeNames -contains 'QuietCli.Projection' -and $data.PSObject.Properties['Omitted']){ $reasons.Add('ProjectionItemLimit');foreach($property in $data.Omitted.PSObject.Properties){$omitted+=[long]$property.Value} } if($data -is [Collections.IEnumerable] -and $data -isnot [string] -and $data -isnot [Collections.IDictionary]){ $all=@($data);if($all.Count -gt $MaxItems){$data=@($all|Select-Object -First $MaxItems);$omitted+=$all.Count-$MaxItems;$reasons.Add('ItemLimit')} } $state=@{Limited=$false;Redacted=$false};$safeData=Protect-QuietValue $data $state;$errorInfo=Protect-QuietValue $result.Error $state $execution=[ordered]@{} foreach($name in @('ExitCode','ElapsedMs','Steps','Issues')){if($result.Execution.PSObject.Properties[$name]){$execution[$name]=Protect-QuietValue $result.Execution.$name $state}} $document=[ordered]@{SchemaVersion=2;Summary=(Protect-QuietValue $result.Summary $state);Outcome=$result.Outcome;Data=$safeData Execution=[pscustomobject]$execution;Completeness=$result.Completeness;Error=$errorInfo Truncation=[ordered]@{Applied=($reasons.Count -gt 0);Reasons=@($reasons);Omitted=$omitted}} if($result.Outcome -ne 'Success' -and $result.Capture -and $ExcerptLines){ $excerpt=[ordered]@{} foreach($stream in @('stdout','stderr')){ $text=if($result.Capture.CaptureComplete){$result.Capture.ReadText($stream)}else{$result.Capture.ReadTail($stream) -join [char]10} if($text){$tail=@(($text -split '\r?\n')|Select-Object -Last $ExcerptLines|ForEach-Object{if($_.Length -gt 1000){$_.Substring(0,1000)+'…'}else{$_}});$excerpt[$stream]=Protect-QuietValue $tail $state} } if($excerpt.Count){$document['Excerpt']=$excerpt} } if($settings.Recall -and $result.Internal.Sources.Count -and !$result.RecordId){Save-QuietRecord $result|Out-Null} if($result.RecordId){$document['RecordId']=$result.RecordId} if($state.Limited){$reasons.Add('ProtectionLimit')} $document.Truncation.Applied=($reasons.Count -gt 0);$document.Truncation.Reasons=@($reasons|Select-Object -Unique) if($Envelope -eq 'compact'){ if(!$document.Error){$document.Remove('Error')} if(!$document.Truncation.Applied){$document.Remove('Truncation')} if($result.Completeness.Source -and $result.Completeness.Analysis -and $result.Completeness.Capture){$document.Remove('Completeness')} } $yaml=$AsYaml -or (!$AsJson -and $settings.OutputFormat -eq 'yaml') $serialize={if($yaml){ConvertTo-QuietYaml ([pscustomobject]$document)}else{ConvertTo-Json -InputObject $document -Depth 64 -Compress}} $text=& $serialize if([Text.Encoding]::UTF8.GetByteCount($text) -gt $MaxOutputBytes){ $document.Data=$null;$document['Truncation']=@{Applied=$true;Reasons=@($reasons)+@('OutputBudget');Omitted=$omitted} $document.Remove('Excerpt');$text=& $serialize if([Text.Encoding]::UTF8.GetByteCount($text) -gt $MaxOutputBytes){$document.Remove('Summary');$document.Execution=@{ExitCode=$result.Execution.ExitCode};$document.Error=if($result.Error){@{Code=$result.Error.Code}}else{$null};$text=& $serialize} if([Text.Encoding]::UTF8.GetByteCount($text) -gt $MaxOutputBytes){$document=[ordered]@{SchemaVersion=2;Outcome=$result.Outcome;Truncation=@{Applied=$true;Reasons=@('MetadataLimit')}};$text=& $serialize} } $result.Internal.PresentationMs=$deliveryWatch.Elapsed.TotalMilliseconds Save-QuietDelivery $result $text $document $text } } function ConvertTo-QuietJson { [CmdletBinding()]param([Parameter(Mandatory,ValueFromPipeline)]$InputObject,[int]$MaxOutputBytes=4096,[int]$MaxItems=20) process{Out-QuietResult -InputObject $InputObject -AsJson -MaxOutputBytes $MaxOutputBytes -MaxItems $MaxItems} } |