src/typesystem/TypeIndex.ps1
# Copyright 2021, Adam Edwards # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. . (import-script TypeIndexEntry) . (import-script TypeMatch) enum TypeIndexClass { Name Property NavigationProperty Method } enum TypeIndexLookupClass { Exact StartsWith Contains } ScriptClass TypeIndex { $IndexedField = $null $index = $null $typeClassAggregates = $null function __initialize([TypeIndexClass] $indexedField) { $this.indexedField = $indexedField # Note on using keyed collections -- see this PowerShell defect: https://github.com/PowerShell/PowerShell/issues/7758. This is due to PowerShell # magical syntactic sugar applied to collections -- if you try to access an instance property of a collection type, PowerShell first # tries to find an item with that key name in the collection (presumably only if the collection has string keys)! Specifically this means # that if you attempt to access the `Keys` property of a collection to enumerate its keys, it will work as expected *unless* you add a # key to the collection that is itself the value string 'Keys'. In that case, you'll get the value associated with that key in the collection rather than # the actual Keys property of the collection object. We hit an issue in this codebase for this specific keyed collection when a property was # added to an entity in the beta API version called 'keys.' :( The workaround: use the 'get_Keys()' method instead of the 'keys' property. # This extends to any other properties of the collection as well such as Count, Length, Values. This is truly a wild problem and extremely # unfortunate design choice to prioritize questionable syntactic usability over predictability. In the case for this object, this was encountered # as a runtime defect after years of the code running just fine (the Graph API schema change added the property that exposed the flaw). # # So call to action: use get_xxxx() everywhere for keyed collections (especially if the key is a string!) in place of any properties of the collection. $this.index = [System.Collections.Generic.SortedList[String, Object]]::new(([System.StringComparer]::OrdinalIgnoreCase)) $this.typeClassAggregates = @{ Entity = 0 Complex = 0 Enumeration = 0 } } function Add([string] $lookupValue, $typeId, $typeClass) { $entry = __FindEntry $lookupValue if ( ! $entry ) { $entry = new-so TypeIndexEntry $lookupValue $this.index.Add($lookupValue, $entry) } $entry.AddTarget($typeId, $typeClass) if ( $this.typeClassAggregates.ContainsKey($typeClass.tostring()) ) { $this.typeClassAggregates[$typeClass.tostring()] += 1 } } function Get($key) { if ( ! $this.index.ContainsKey($key) ) { throw "Key '$key' not found for index '$($this.indexedField)'" } __FindEntry $key } function GetLookupValues { $this.index.get_Keys() } function Find($key, $typeClasses) { $entry = __FindEntry $key if ( $entry ) { foreach ( $matchingType in $entry.targets.get_Keys() ) { $matchedTypeClass = $entry.targets[$matchingType] if ( ! $typeClasses -or ( $typeClasses -contains $matchedTypeClass ) ) { new-so TypeMatch $this.indexedField $key $matchingType $matchedTypeClass @($key) } } } } function FindStartsWith($searchString, $typeClasses) { $normalizedSearchString = $searchString.tolower() $matchedValues = $this.index.get_Keys() | where { $_.tolower().StartsWith($normalizedSearchString) } if ( $matchedValues ) { foreach ( $matchingvalue in $matchedValues ) { $entry = $this.index[$matchingValue] foreach ( $matchingType in $entry.targets.get_Keys() ) { $matchedTypeClass = $entry.targets[$matchingType] if ( ! $typeClasses -or ( $typeClasses -contains $matchedTypeClass ) ) { new-so TypeMatch $this.indexedField $searchString $matchingType $matchedTypeClass $matchedValues } } } } } function FindContains($searchString, $typeClasses) { $normalizedSearchString = $searchString.tolower() $matchedValues = $this.index.get_keys() | where { $_.tolower().Contains($normalizedSearchString) } if ( $matchedValues ) { foreach ( $matchingvalue in $matchedValues ) { $entry = $this.index[$matchingValue] foreach ( $matchingType in $entry.targets.get_keys() ) { $matchedTypeClass = $entry.targets[$matchingType] if ( ! $typeClasses -or ( $typeClasses -contains $matchedTypeClass ) ) { new-so TypeMatch $this.indexedField $searchString $matchingType $matchedTypeClass @($matchingValue) } } } } } function GetStatistics { [PSCustomObject] @{ EntityCount = $this.typeClassAggregates['Entity'] ComplexCount = $this.typeClassAggregates['Complex'] EnumerationCount = $this.typeClassAggregates['Enumeration'] } } function __FindEntry($key) { $this.index[$key] } } |