Jsonify.psm1
using namespace System.Collections.Generic using namespace System.Collections using namespace System.Management.Automation.Language using namespace System.Management.Automation using namespace System.Management # ModuleConfig # ExportCoercionFunctions $script:DefaultTemplateTypeMapping = @{} $script:ModuleConfig = @{ ExportCoercionFunctions = $true ExportDebugFunctions = $true AlwaysWarnWhenTypeNotFound = $True VerboseSettings = $true } if($ModuleConfig.VerboseSettings) { $PSDefaultParameterValues['Set-JsonifyDefaultCoerceTemplate:Verbose'] = $true } function Get-JsonifyDateTimeFormatString { <# .SYNOPSIS Quickly get and preview named date format string patterns, for a specific culture, else the system's current culture. .EXAMPLE Get-JsonifyDateTimeFormatString .EXAMPLE Get-JsonifyDateTimeFormatString -CultureName 'en-GB'|fl #> param( [ArgumentCompletions('en-US', 'en-GB', 'de-DE', 'es-ES')] [string]$CultureName ) if( -not $CultureName ) { $Cult = Get-Culture } else { $Cult = Get-Culture $CultureName -ea 'stop' } # [System.DateTimeOffset]::Now.ToString( $cult.DateTimeFormat.LongDatePattern ) $Cult.DateTimeFormat.psobject.properties.Where{$_.Name -match 'Pattern'} | Sort-Object Name | %{ $Name = $_.Name $fStr = $_.Value $Ex_dt = try { [Datetime]::Now.ToString( $fStr, $Cult ) } catch { "Failed: $_" } $Ex_dto = try { [DateTimeOffset]::Now.ToString( $fStr, $Cult ) } catch { "Failed: $_" } [pscustomobject]@{ PSTypeName = 'Jsonify.Named.DateTimeFormat.Patterns' Name = $Name FormatStr = $fStr Datetime = $Ex_dt DateTimeOffset = $ex_dto } } } function Get-JsonifyConfig { <# .SYNOPSIS Read Module level options .LINK Get-JsonifyConfig .LINK Set-JsonifyConfig #> param() $state = $script:ModuleConfig return $state } function Set-JsonifyConfig { <# Set Module level options .example Pwsh🐒 # confirm settings changed by causing a warning > CoerceFrom.AnyType ( ( gi fg:\red ) ) WARNING: No automatic type detected RgbColor Pwsh🐒 > $cfg = Get-JsonifyConfig > $cfg.AlwaysWarnWhenTypeNotFound = $false > Set-JsonifyConfig $cFg > CoerceFrom.AnyType ( ( gi fg:\red ) ) # no warning .LINK Get-JsonifyConfig .LINK Set-JsonifyConfig #> param( [Alias('Config')] [hashtable]$newConfig ) $state = $script:ModuleConfig $state = $newConfig } # class SummarizePropertyRecord { # [PSMemberTypes]$MemberType = [PSMemberTypes]::All # [object]$FullName # [string]$ShortName # # SummarizePropertyRecord( $Options ) { # # } # } # function js.Summarize.SingleProperty { # param( # $InputObject # ) # $meta = @{ # Name = 'Name' # MemberKind = # } # [PSMemberTypes] # [pscustomobject]$meta # } # function js.DescribeType { # param( # $InputObject # ) # $Obj = $InputObject # $TrueNull = $Null = $Obj # $Tinfo = ($Obj)?.GetType() # $meta = [ordered]@{ # PSTypeName = 'jsonify.{0}.Record' -f $MyInvocation.MyCommand.Name # Count = $Obj.Count # Len = $Obj.Length # Is = [ordered]@{ # Null = $Null -eq $Obj # EmptyStr = [String]::IsNullOrEmpty( $Obj ) # Blank = [String]::IsNullOrWhiteSpace( $Obj ) # Array = 'nyi' # } # Implements = [ordered]@{ # IList = 'nyi' # ICollection = 'nyi' # IEnumerable = 'nyi' # } # Value = $InputObject # PropsFromType = [List[Object]]::New() # PropsFromObject = [List[Object]]::New() # } # [pscustomobject]$Meta # # update-typedata to hide Value ? # } function WarnIf.NotType { param( [Parameter(Mandatory)] [object]$InputObject, [Parameter(Mandatory)] [string[]]$TypeName ) $state = $script:ModuleConfig if( -not $state.AlwaysWarnWhenTypeNotFound ) { return } $matchAny = $false $tinfo = $InputObject.GetType() # todo: refactor and combine the 'is type equial-ish-to-type-name' used in the other functions too foreach($curType in @( $TypeName) ) { # sometimes type instances are instantiable, but this will break. use strings instead. # $directTypeInstance? = $curType -as 'type' # if( $directTypeInstance? ) { # if( $InputObject -is $DirectTypeIstance?) { $matchAny = $true } # } if($InputObject -is 'string' -and ($InputObject -eq $curType)) { $matchAny = $true } if($InputObject -eq $curType) { $matchAny = $true } # if($InputObject -is ($curType -as 'type') ) { $matchAny = $true } if($tinfo.Name -eq $curType) { $matchAny = $true } # if($InputObject.) } if($MatchAny) { return } 'Jsonify: WarnIfNotType: expected {0} but found {1}' -f @( $typeName -join ', ' $InputObject.GetType().Name ) | write-warning } function Set-JsonifyDefaultTypeTemplate { <# .SYNOPSIS set which templates are default globally, to use automatically .NOTES no validation #> [Alias('Set-JsonifyDefaultCoerceTemplate')] [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrWhiteSpace()] [string[]]$TypeName, [Alias('Name')][Parameter(Mandatory)] [ValidateNotNullOrWhiteSpace()] [string]$TemplateName ) $state = $script:DefaultTemplateTypeMapping foreach($curTypeName in @($TypeName) ) { $prevState = $state.ContainsKey( $curTypeName ) ? $state[ $curTypeName ] : 'NeverSet' $state[ $curTypeName ] = $TemplateName 'SetDefaultTemplateType: Type: {0}, Was: {1}, new: {2}' -f @( $curTypeName $prevState $state[ $curTypeName ] ) | write-verbose } } function __Get-FirstTemplateName { # __Get-DefaultTemplateName <# .synopsis Returns at most, one match. Should this be cached or fast testing relative commandlet? or is that overkill .NOTES #> [OutputType('System.String')] param( [string]$TypeName, [switch]$UsingWildcard # should compares be equal to, or like, skipping regex match? # [switch]$TypeAsLiteral ) # if missing, emit nothing, or false? return @(Get-JsonifyDefaultCoerceTemplate -TypeName $TypeName -UsingWildcard:$UsingWildcard).Where({$_},'first').TemplateName } function Get-JsonifyDefaultTypeTemplate { <# .SYNOPSIS set which templates are default globally, to use automatically .notes Exact matches are returned first, regardless of match mode .EXAMPLE Get-JsonifyDefaultCoerceTemplate -All # using regex compare Get-JsonifyDefaultCoerceTemplate -TypeName file Get-JsonifyDefaultCoerceTemplate -TypeName dir, file .EXAMPLE # -like compare Get-JsonifyDefaultTypeTemplate -TypeName *e -AsWildcard .NOTES no validation #> [Alias('Get-JsonifyDefaultCoerceTemplate')] [CmdletBinding(DefaultParameterSetName='ByNames')] param( # find exact or regex matches [Parameter(Mandatory, Position=0, ParameterSetName='ByNames')] [ValidateNotNullOrWhiteSpace()] [string[]]$TypeName, [Alias('All', 'List')] [Parameter(ParameterSetName='ListOnly')] [switch]$ListAll, # Do I return only key names? [Alias('Keys')] [Parameter(ParameterSetName='ListOnly')] [switch]$KeysOnly, # use -like compares for type names, rather than -match [Alias('Wildcard', 'AsWildcard')] [switch]$UsingWildcard ) # // todo future: $state should be config instances enum JsonifyTypeConfigMatchKind { Exact Wildcard Regex None All } class JsonifyDefaultTypeConfig { # PSTypeName = 'Jsonify.Config.DefaultTypeTemplate.Record' [string]$TypeName = '' [string]$TemplateName = '' [string]$MatchKind = 'None' # hidden hidden [bool]$ExactMatch = $false } # // future: $state should be config instances # [JsonifyDefaultTypeConfig]@{ # TypeName = $Key # TemplateName = $state[ $Key ] # } $Conf = @{ AlwaysStripSystemNamespace = $True } $state = $script:DefaultTemplateTypeMapping # if($ListAll) { # return $state # } $query = @( foreach($Key in $state.Keys.Clone()) { $thisKeyMatchedSomething = $false if($ListAll){ $thisKeyMatchedSomething = $true [JsonifyDefaultTypeConfig]@{ TypeName = $Key TemplateName = $state[ $Key ] MatchKind = [JsonifyTypeConfigMatchKind]::All } continue } foreach($Name in $TypeName) { $curMatchKind = [JsonifyTypeConfigMatchKind]::None if($Conf.AlwaysStripSystemNamespace) { $Name = $Name -replace '^System\.', '' } # if($thisKeyMatchedSomething) { break } if( $UsingWildcard -and ($key -like $name)) { $thisKeyMatchedSomething = $true $curMatchKind = [JsonifyTypeConfigMatchKind]::Wildcard } if( ( -not $UsingWildcard) -and ($Key -match $Name)) { $thisKeyMatchedSomething = $true $curMatchKind = [JsonifyTypeConfigMatchKind]::Regex } if( $key -eq $Name ) { $thisKeyMatchedSomething = $true $curMatchKind = [JsonifyTypeConfigMatchKind]::Exact } if($thisKeyMatchedSomething) { [JsonifyDefaultTypeConfig]@{ TypeName = $Key TemplateName = $state[ $Key ] MatchKind = $curMatchKind ExactMatch = $matchKind -eq [JsonifyTypeConfigMatchKind]::Exact } # [pscustomobject]@{ # PSTypeName = 'Jsonify.Config.DefaultTypeTemplate.Record' # TypeName = $Key # TemplateName = $state[ $Key ] # } # $thisKeyMatchedSomething = $true break } } # $state.Keys }) $query = $query | sort-Object -Unique ExactMatch, TypeName, TemplateName if($KeysOnly) { return $query.TypeName } return $query } function CoerceFrom.Datetime { [OutputType('System.string')] param( [Parameter(mandatory, Position=0, ValueFromPipeline)] [object]$InputObject, [ArgumentCompletions( 'Basic', 'o', 'YearMonthDay' )] [string]$TemplateName = 'o', # [Microsoft.PowerShell.Commands.ValidateCultureNamesGenerator()] [object]$CultureName # [Alias('IgnoreProp', 'DropProperty', 'Drop')] # [string[]]$ExcludeProperty = @() ) begin { # CoerceFrom.FileSystemInfo (gi .\readme.md) if($CultureName) { $CultInfo = Get-Culture $CultureName -ea 'ignore' } } process { $tinfo = ( $InputObject )?.GetType() # WarnIf.NotType $InputObject -type 'DateTime' # todo: refactor and combine this plus WarnIf.NotType the 'is type equial-ish-to-type-name' used in the other functions too WarnIf.NotType -In $InputObject -type 'DateTime', 'DateTimeOffset' if($PSBoundParameters.ContainsKey('TemplateName')){ $whichTemplate = $TemplateName } else { $whichTemplate = __Get-FirstTemplateName -TypeName $Tinfo.Name } # $which = __Get-FirstTemplateName -TypeName 'Datetime' -UsingWildcard switch( $whichTemplate ) { 'YearMonthDay' { $formatStr = 'yyyy-MM-dd' } 'Basic' { $formatStr = 'o' } default { $formatStr = $which } } if( $CultureInfo ) { return $InputObject.ToString( $formatStr, $CultureInfo ) } else { return $InputObject.ToString( $formatStr ) } } } function CoerceFrom.FileSystemInfo { [Alias('CoerceFrom.File')] param( [object]$InputObject, [ValidateSet( 'Basic', 'Minify' )] [string]$TemplateName = 'Basic', [Alias('IgnoreProp', 'DropProperty', 'Drop')] [string[]]$ExcludeProperty = @( 'PSPath' 'LastAccessTime' 'Exists' # 'Extension' 'PSChildName' 'PSDrive' 'PSIsContainer' 'PSParentPath' 'PSPath' 'PSProvider' 'ResolvedTarget' 'Target' 'UnixFileMode' 'Attributes' 'Mode' 'IsReadOnly' # 'Length' 'VersionInfo' ) ) $Obj = $InputObject $tinfo = $InputObject.GetType() $meta = [ordered]@{ PSTypeName = 'Jsonify.File' TypeName = $Tinfo.Name } 'AutoJson using TemplateName {0} on {1}' -f @( $TemplateName ; $Obj.GetType().Name ) | write-verbose switch( $tinfo.FullName ) { { $_ -in @( 'System.IO.DirectoryInfo' ) } { # Shared props $meta.Name = $Obj.Name.ToString() $meta.BaseName = $Obj.Name.ToString() $meta.FullName = $Obj.FullName.ToString() $meta.PSPath = $Obj.PSPath.ToString() $meta.Length = $Obj.Length $meta.CreationTime = CoerceFrom.Datetime $Obj.CreationTime $meta.CreationTimeUtc = CoerceFrom.Datetime $Obj.CreationTimeUtc $meta.LastWriteTime = CoerceFrom.Datetime $Obj.LastWriteTime $meta.LastWriteTimeUtc = CoerceFrom.Datetime $Obj.LastWriteTimeUtc $meta.LastAccessTime = CoerceFrom.Datetime $Obj.LastWriteTime $meta.LastAccessTimeUtc = CoerceFrom.Datetime $Obj.LastWriteTimeUtc $meta.Attributes = [string]$Obj.Attributes $meta.Exists = [string]$Obj.Exists $meta.Extension = [string]$Obj.Extension $meta.LinkTarget = [string]$Obj.LinkTarget $meta.LinkType = [string]$Obj.LinkType $meta.Mode = [string]$Obj.Mode $meta.ModeWithoutHardLink = [string]$Obj.ModeWithoutHardLink $meta.PSChildName = [string]$Obj.PSChildName $meta.PSDrive = [string]$Obj.PSDrive $meta.PSIsContainer = [string]$Obj.PSIsContainer $meta.PSParentPath = [string]$Obj.PSParentPath $meta.PSPath = [string]$Obj.PSPath $meta.PSProvider = [string]$Obj.PSProvider $meta.ResolvedTarget = [string]$Obj.ResolvedTarget $meta.Target = [string]$Obj.Target $meta.UnixFileMode = [string]$Obj.UnixFileMode } 'System.IO.DirectoryInfo' { <# props built by command Dot.List.Contains ( aj.Props (get-item .) -NameOnly) (aj.Props (get-item .\get-item-auto.json) -NameOnly) A.NotIn.B #> $meta.Parent = [string]$Obj.Parent $meta.Root = [string]$Obj.Root } 'System.IO.FileInfo' { <# props built by command Dot.List.Contains ( aj.Props (get-item .) -NameOnly) (aj.Props (get-item .\get-item-auto.json) -NameOnly) A.NotIn.B Directory DirectoryName IsReadOnly Length VersionInfo #> $meta.Directory = [string]$Obj.Directory $meta.DirectoryName = [string]$Obj.DirectoryName $meta.IsReadOnly = [string]$Obj.IsReadOnly $meta.Length = [string]$Obj.Length $meta.VersionInfo = [string]$Obj.VersionInfo } default { throw "AutoJsonify.From.FilesSystemInfo::UnhandledType: $( $InputObject.GetType() )" } } switch($TemplateName) { 'Basic' { #n o-op atm } 'Minify' { # remove almost all properties $toRemove = $meta.Keys.clone().where{ $_ -notin @( 'Name' 'FullName' 'Length' 'LastWriteTimeUtc' ) } $ExcludeProperty = @( $ExcludeProperty ; $toRemove ) write-debug 'minify using template' } default { throw "UnhandledTemplateName: $TemplateName"} } # $simplfiy foreach($name in @( $ExcludeProperty )) { $meta.remove( $Name ) } [pscustomobject]$meta } function CoerceFrom.AnyType { <# .SYNOPSIS Delegate to custom type handlers #> [CmdletBinding()] [OutputType('System.String')] [Alias('CoerceFrom.Any', 'CoerceFrom.Object')] param( [object]$InputObject ) $tinfo = $InputObject.GetType() $longName = $tinfo.Namespace, $tinfo.Name -join '.' #-replace '^System\.', '' # $longName = $tinfo.Namespace, $tinfo.Name -join '.' -replace '^System\.', '' # $fullName# [string]$TypeName = $InputObject.GetType().FullName # $shortName = $Tinfo.Name switch( $longName ) { 'System.IO.FileSystemInfo' { $new = CoerceFrom.FileSystemInfo -InputObject $InputObject } 'System.DateTime' { $new = CoerceFrom.Datetime -InputObject $InputObject } default { $new = $InputObject 'No automatic type detected {0}' -f @( $tinfo.Name ) | write-debug if($ModuleConfig.AlwaysWarnWhenTypeNotFound) { 'No automatic type detected {0}' -f @( $tinfo.Name ) | write-warning } } } # if($_ -is 'IO.FileSystemInfo') { # $new = CoerceFrom.FileSystemInfo -InputObject $InputObject # } else { # $new = $InputObject # } return $new } function Jsonify.GetCommands { param( $InputObject ) gcm -m Ninmonkey.Console *jsonif* -ea 'ignore' | Join-string -sep ', ' -op 'see also: Ninmonkey.Console\ => [ ' -os ' ] ' | Write-verbose -verbose (Get-module 'Jsonify' ).ExportedCommands | Join-String -sep ', ' -op 'Commands: ' | write-Verbose } function ConvertTo-Jsonify { <# .SYNOPSIS core entry point for the proxy of ConvertTo-Json #> [Alias( 'Jsonify' # 'AutoJsonify' # 'Jsonify', 'aj.Json' )] [OutputType('Object', ParameterSetName='__AllParameterSets')] [OutputType('System.String', ParameterSetName='outJson')] param( [Alias('Obj', 'Data', 'InpObj', 'In')] [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [AllowNull()] [Object]${InputObject}, [ValidateRange(0, 100)] [int]${Depth} = 6, [switch]${Compress}, [switch]${EnumsAsStrings} = $true, [switch]${AsArray} = $true, #[Newtonsoft.Json.StringEscapeHandling] [ValidateSet( 'Default', 'EscapeNonAscii', 'EscapeHtml' )] ${EscapeHandling}) # if false, objects that will coerce nicely to json are emitted # letting you operate over objects # if true, invokes json conversion here # Or use the global alias 'ConvertTo-Json' which will directly convert at the end [Parameter(ParameterSetName='OutJson')] [Alias('AsJson')] [switch]$OutJson begin { try { # $outBuffer = $null # if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { # $PSBoundParameters['OutBuffer'] = 1 # } $newParams = [ordered]@{} + $PSBoundParameters $newParams['WarningAction'] = 'ignore' $commandName = 'Microsoft.PowerShell.Utility\ConvertTo-Json' $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand( <# commandName: #> $commandName, <# type: #> [CommandTypes]::Cmdlet ) if($PSBoundParameters.ContainsKey('Compress')) { $newParams['Compress'] = $Compress } if($PSBoundParameters.ContainsKey('Depth')) { # or always? it depends whether this controls itself, or, sub invokes. $newParams['Depth'] = $Depth } $scriptCmd = { & $wrappedCmd @newParams } $steppablePipeline = $scriptCmd.GetSteppablePipeline( $myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { # write-verbose 'Aj.Json :: Process' $new = CoerceFrom.AnyType -InputObject $_ if($OutJson) { $new = $new | ConvertTo-Json -Compress:$Compress -Depth:$Depth } $steppablePipeline.Process( $new ) } catch { throw } } end { # write-verbose 'Aj.Json :: End' try { $steppablePipeline.End() } catch { throw } } clean { if ($null -ne $steppablePipeline) { $steppablePipeline.Clean() } } } Export-ModuleMember -Function @( 'Get-Jsonify*' 'Set-Jsonify*' 'ConvertTo-Jsonify' # 'Get-JsonifyDefaultTypeTemplate' # 'Get-JsonifyDefaultCoerceTemplate' 'Set-JsonifyDefaultCoerceTemplate' 'Get-JsonifyDefaultTypeTemplate' 'Get-JsonifyDefaultCoerceTemplate' 'ConvertTo-Jsonify' 'AutoJson.*' 'AutoJsonify.*' 'aj.*' 'Jsonify.*', 'Json.*' if( $ModuleConfig.ExportCoercionFunctions ) { 'CoerceFrom.*' } if( $ModuleConfig.ExportDebugFunctions ) { 'WarnIf.NotType' } ) -Alias @( 'Jsonify' 'Get-Jsonify*' 'Set-Jsonify*' 'AutoJson.*' 'AutoJsonify.*' 'aj.*' 'Jsonify.*', 'Json.*' if( $ModuleConfig.ExportCoercionFunctions ) { 'CoerceFrom.*' } ) # # no validation Set-JsonifyDefaultCoerceTemplate -TypeName 'IO.FileSystemInfo' -TemplateName 'Minfiy' Set-JsonifyDefaultCoerceTemplate -TypeName 'IO.FileSystemInfo' -TemplateName 'Minify' Set-JsonifyDefaultCoerceTemplate -TypeName 'IO.DirectoryInfo' -TemplateName 'Minify' Set-JsonifyDefaultCoerceTemplate -TypeName 'IO.FileInfo' -TemplateName 'Minify' Set-JsonifyDefaultCoerceTemplate -TypeName 'DateTime' -TemplateName 'o' Set-JsonifyDefaultCoerceTemplate -TypeName 'DateTimeOffset' -TemplateName 'YearMonthDay' # Set-JsonifyDefaultCoerceTemplate -TypeName 'System.IO.FileSystemInfo' -TemplateName 'Minify' # Set-JsonifyDefaultCoerceTemplate -TypeName 'System.IO.DirectoryInfo' -TemplateName 'Minify' # Set-JsonifyDefaultCoerceTemplate -TypeName 'System.IO.FileInfo' -TemplateName 'Minify' # Set-JsonifyDefaultCoerceTemplate -TypeName 'System.DateTime' -TemplateName 'o' |