From 8ddd2b74dcca2e11b1d98b4ee677efe6fd27baff Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Tue, 4 Aug 2026 15:29:29 -0700 Subject: [PATCH 01/13] fix(wrapper-generator): map numeric parameter types by OpenAPI format Graph declares Edm.Int32/Int64 as "number" with the real type in the format; mapping by type alone emitted double? against Kiota's int? and did not compile. An explicit format now decides the CLR type, mirroring Kiota's own mapping. --- .../SchemaPropertiesTests.cs | 9 +++++++++ tools/WrapperGenerator/SchemaProperties.cs | 20 +++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs index 5ee8eef3c39..7ac2b68961a 100644 --- a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs +++ b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs @@ -87,6 +87,12 @@ public void MapsNumericFormatsWithoutDataLoss() ["sizeInBytes"] = Scalar(JsonSchemaType.Integer, format: "int64"), // values > 2^31 must survive ["retryCount"] = Scalar(JsonSchemaType.Integer, format: "int32"), ["plainCount"] = Scalar(JsonSchemaType.Integer), + // Graph's docs declare Edm.Int32/Int64 as type "number" with the format carrying + // the real type (mailFolder.childFolderCount, messageRule.sequence). The format + // must win or the parameter type contradicts the Kiota model and won't compile. + ["childFolderCount"] = Scalar(JsonSchemaType.Number, format: "int32"), + ["quotaUsed"] = Scalar(JsonSchemaType.Number, format: "int64"), + ["confidence"] = Scalar(JsonSchemaType.Number, format: "float"), }, }; @@ -96,6 +102,9 @@ public void MapsNumericFormatsWithoutDataLoss() Assert.Equal("long", props.Single(p => p.OpenApiName == "sizeInBytes").PsTypeName); Assert.Equal("int", props.Single(p => p.OpenApiName == "retryCount").PsTypeName); Assert.Equal("int", props.Single(p => p.OpenApiName == "plainCount").PsTypeName); + Assert.Equal("int", props.Single(p => p.OpenApiName == "childFolderCount").PsTypeName); + Assert.Equal("long", props.Single(p => p.OpenApiName == "quotaUsed").PsTypeName); + Assert.Equal("float", props.Single(p => p.OpenApiName == "confidence").PsTypeName); } [Fact] diff --git a/tools/WrapperGenerator/SchemaProperties.cs b/tools/WrapperGenerator/SchemaProperties.cs index 59269c8c9e9..08fe5d4b3e3 100644 --- a/tools/WrapperGenerator/SchemaProperties.cs +++ b/tools/WrapperGenerator/SchemaProperties.cs @@ -67,16 +67,24 @@ public static bool HasPasswordProfile(IOpenApiSchema schema) _ => false, }; - // Numeric mapping follows the OpenAPI format so values survive the round trip: an int64 - // property must not truncate to int (overflow above ~2.1 billion) and a number property - // must not lose its fraction to integer truncation. + // Numeric mapping: when a format is present it decides the CLR type, mirroring Kiota's + // own mapping, so a wrapper parameter always matches the Kiota model property it is + // assigned to. Graph's docs declare Edm.Int32 as "type: number, format: int32" — going by + // the type alone would emit double? against Kiota's int? and not compile. Without a + // format, integer stays int and number stays double (fraction and 64-bit safety). private static string MapPsType(IOpenApiSchema schema) => (schema.Type & ~JsonSchemaType.Null) switch { JsonSchemaType.String => "string", JsonSchemaType.Boolean => "bool", - JsonSchemaType.Integer when string.Equals(schema.Format, "int64", StringComparison.OrdinalIgnoreCase) => "long", - JsonSchemaType.Integer => "int", - JsonSchemaType.Number => "double", + JsonSchemaType.Integer or JsonSchemaType.Number => schema.Format?.ToLowerInvariant() switch + { + "int64" => "long", + "int32" => "int", + "float" => "float", + "double" => "double", + "decimal" => "decimal", + _ => (schema.Type & ~JsonSchemaType.Null) == JsonSchemaType.Integer ? "int" : "double", + }, _ => "string", }; From ca215f3771f981c774d5bd3b5f4092c174676b73 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Mon, 3 Aug 2026 15:39:36 -0700 Subject: [PATCH 02/13] fix(wrapper-generator): correct four singularization words found by oracle audit Auditing every v1.0 GET command in MgCommandMetadata.json against the singularizer surfaced four words where the rules disagree with shipped cmdlet names: Cookies -> "Cooky" (ships as ...HostCookie), Skus kept as-is (ships as Get-MgSubscribedSku), Dns -> "Dn" (ships as Get-MgDomainVerificationDnsRecord), Ios -> "Io" (ships as Get-MgDeviceAppManagementIosManagedAppProtection). Adds two irregulars and two invariants, each with a pinned test, and refreshes the README test count. 82 tests passing. Full-inventory match after fix: 796 of 870 noun segments; the remaining 74 are action/function segments and AutoRest hand renames, tracked separately. --- tools/WrapperGenerator.Tests/NamingTests.cs | 10 ++++++++-- tools/WrapperGenerator/README.md | 2 +- tools/WrapperGenerator/Singularizer.cs | 10 +++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tools/WrapperGenerator.Tests/NamingTests.cs b/tools/WrapperGenerator.Tests/NamingTests.cs index c466606d909..902a4568a19 100644 --- a/tools/WrapperGenerator.Tests/NamingTests.cs +++ b/tools/WrapperGenerator.Tests/NamingTests.cs @@ -29,11 +29,17 @@ public sealed class SingularizerTests [InlineData("Plans", "Plan")] [InlineData("Settings", "Setting")] [InlineData("Licenses", "License")] - // irregulars (Get-MgDriveItemChild, Get-MgUserPerson) + // irregulars (Get-MgDriveItemChild, Get-MgUserPerson, + // Get-MgSecurityThreatIntelligenceHostCookie, Get-MgSubscribedSku) [InlineData("Children", "Child")] [InlineData("People", "Person")] - // invariants (Get-MgUserSettingWindows) + [InlineData("Cookies", "Cookie")] + [InlineData("Skus", "Sku")] + // invariants (Get-MgUserSettingWindows, Get-MgDomainVerificationDnsRecord, + // Get-MgDeviceAppManagementIosManagedAppProtection) [InlineData("Windows", "Windows")] + [InlineData("Dns", "Dns")] + [InlineData("Ios", "Ios")] // acronyms are never plural forms [InlineData("OS", "OS")] public void SingularizesWords(string word, string expected) diff --git a/tools/WrapperGenerator/README.md b/tools/WrapperGenerator/README.md index aa348a69d00..185f50236f4 100644 --- a/tools/WrapperGenerator/README.md +++ b/tools/WrapperGenerator/README.md @@ -157,7 +157,7 @@ dotnet run --project tools/WrapperGenerator -- ` ```powershell # 1. Naming rules pinned to published Microsoft.Graph names (69 tests) dotnet test tools/WrapperGenerator.Tests -# => Passed! - Failed: 0, Passed: 69, Total: 69 +# => Passed! - Failed: 0, Passed: 82, Total: 82 # 2. Parity gate: generate, then check every cmdlet name against Graph's own command inventory .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath diff --git a/tools/WrapperGenerator/Singularizer.cs b/tools/WrapperGenerator/Singularizer.cs index c00957ecc81..ab67a0aa412 100644 --- a/tools/WrapperGenerator/Singularizer.cs +++ b/tools/WrapperGenerator/Singularizer.cs @@ -18,17 +18,25 @@ namespace WrapperGenerator; public static partial class Singularizer { // Irregular plurals the SDK singularizes: Get-MgDriveItemChild, Get-MgUserPerson. + // "Cookies" would hit the ies-rule ("Cooky") but ships as Get-MgSecurityThreatIntelligenceHostCookie; + // "Skus" would hit the us-guard (stay put) but ships as Get-MgSubscribedSku. private static readonly Dictionary Irregulars = new(StringComparer.Ordinal) { ["Children"] = "Child", ["People"] = "Person", + ["Cookies"] = "Cookie", + ["Skus"] = "Sku", }; // Words that end in "s" but are not plurals. The SDK keeps them as-is: - // /users/{id}/settings/windows ships as Get-MgUserSettingWindows. + // /users/{id}/settings/windows ships as Get-MgUserSettingWindows, verificationDnsRecords + // as Get-MgDomainVerificationDnsRecord, iosManagedAppProtections as + // Get-MgDeviceAppManagementIosManagedAppProtection. private static readonly HashSet Invariants = new(StringComparer.Ordinal) { "Windows", + "Dns", + "Ios", }; // Splits Pascal or camel text into words. Handles acronym runs ("OS" in "MacOSDmgApp"), From c4d854594484a759a38e104cd6064c7f1fb7bdb5 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Tue, 4 Aug 2026 15:29:20 -0700 Subject: [PATCH 03/13] fix(wrapper-generator): correct HostWhoi name, add Statistics invariant, start edge-case catalog Only 2 of 30 shipped whois-family commands truncate "Whois" to "Whoi"; per review decision the generator emits the corrected ...HostWhois (no alias for the old name), and the parity gate reports it as [CORRECTED] instead of failing. "Statistics" joins the invariants, found via the DEVX Humanizer exception list. edge-cases/naming-edge-cases.md starts the per-class catalog of naming defects. 88 tests passing. --- tools/Compare-WrapperCmdletNames.ps1 | 35 ++++- tools/WrapperGenerator.Tests/NamingTests.cs | 30 +++- tools/WrapperGenerator/README.md | 8 +- tools/WrapperGenerator/Singularizer.cs | 8 +- .../edge-cases/naming-edge-cases.md | 138 ++++++++++++++++++ 5 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 tools/WrapperGenerator/edge-cases/naming-edge-cases.md diff --git a/tools/Compare-WrapperCmdletNames.ps1 b/tools/Compare-WrapperCmdletNames.ps1 index fcf8c7a28c9..e06f24538c6 100644 --- a/tools/Compare-WrapperCmdletNames.ps1 +++ b/tools/Compare-WrapperCmdletNames.ps1 @@ -12,6 +12,11 @@ Method+Uri -> Command inventory in MgCommandMetadata.json, and reports whether t emitted [Cmdlet(...)] name matches what the oracle says the published SDK calls that operation. +A small set of published names are known AutoRest defects the generator deliberately +corrects instead of reproducing (tools/WrapperGenerator/edge-cases/naming-edge-cases.md +is the catalog). Those are matched against the $deliberateCorrections table below and +reported as [CORRECTED] rather than [MISMATCH]; they do not fail the gate. + Dispatcher cmdlets (the paired-GET public cmdlet that only forwards to its internal _List/_Get siblings via InvokeCommand.InvokeScript - see CmdletEmitter.EmitGetDispatcher) contain no direct Graph call, so there is nothing to reconstruct from their source; they @@ -126,6 +131,17 @@ function Get-ModuleApiVersion { return $null } +# Published names the generator deliberately corrects instead of reproducing. Each entry maps +# the shipped (wrong) command to the corrected one the generator emits, and must have a matching +# entry in tools/WrapperGenerator/edge-cases/naming-edge-cases.md and a pinned naming test. The +# gate reports these as [CORRECTED] instead of [MISMATCH] and does not fail on them. +$deliberateCorrections = @{ + # AutoRest inflected the trailing /whois segment to "Whoi"; the other 28 whois-family + # cmdlets (whoisRecords, whoisHistoryRecords) all keep "Whois". + 'Get-MgSecurityThreatIntelligenceHostWhoi' = 'Get-MgSecurityThreatIntelligenceHostWhois' + 'Get-MgBetaSecurityThreatIntelligenceHostWhoi' = 'Get-MgBetaSecurityThreatIntelligenceHostWhois' +} + Write-Host "Loading oracle from $OraclePath ..." $oracle = Get-Content -Path $OraclePath -Raw | ConvertFrom-Json @@ -174,6 +190,7 @@ $totalMatched = 0 $totalMismatches = 0 $totalDispatchers = 0 $totalUnparseable = 0 +$totalCorrected = 0 foreach ($module in $modules | Sort-Object Name) { $files = Get-ChildItem -Path $module.Path -Filter '*.g.cs' -File | Sort-Object Name @@ -182,7 +199,9 @@ foreach ($module in $modules | Sort-Object Name) { $moduleMatched = 0 $moduleDispatchers = 0 $moduleUnparseable = 0 + $moduleCorrected = 0 $moduleSkips = @() + $moduleCorrections = @() $moduleProblems = @() foreach ($file in $files) { @@ -238,16 +257,25 @@ foreach ($module in $modules | Sort-Object Name) { $moduleMatched++ } else { - $moduleProblems += " [MISMATCH] $($file.Name): generated '$expectedCommand', oracle says '$($candidates | Select-Object -First 1)' for $method $normalizedUri." + $oracleCommand = $candidates | Select-Object -First 1 + if ($deliberateCorrections[$oracleCommand] -eq $expectedCommand) { + $moduleCorrected++ + $moduleCorrections += " [CORRECTED] $($file.Name): oracle ships '$oracleCommand'; generator deliberately emits '$expectedCommand' (see tools/WrapperGenerator/edge-cases/naming-edge-cases.md)." + } + else { + $moduleProblems += " [MISMATCH] $($file.Name): generated '$expectedCommand', oracle says '$oracleCommand' for $method $normalizedUri." + } } } $status = if ($moduleJoinable -eq 0) { 'n/a' } else { "$moduleMatched of $moduleJoinable" } $dispatcherNote = if ($moduleDispatchers -gt 0) { " (+$moduleDispatchers dispatcher cmdlet(s), no direct call to verify)" } else { '' } $castNote = if ($moduleUnparseable -gt 0) { " (+$moduleUnparseable cast cmdlet(s) skipped, not generated end to end yet)" } else { '' } + $correctedNote = if ($moduleCorrected -gt 0) { " (+$moduleCorrected deliberately corrected name(s))" } else { '' } $versionNote = if ($apiVersion) { " [$apiVersion]" } else { ' [ApiVersion unknown - searched all versions]' } - Write-Host "$($module.Name)$($versionNote): $status cmdlets match the oracle$dispatcherNote$castNote" + Write-Host "$($module.Name)$($versionNote): $status cmdlets match the oracle$dispatcherNote$castNote$correctedNote" foreach ($line in $moduleSkips) { Write-Host $line -ForegroundColor DarkYellow } + foreach ($line in $moduleCorrections) { Write-Host $line -ForegroundColor DarkCyan } foreach ($line in $moduleProblems) { Write-Host $line -ForegroundColor Yellow } $totalJoinable += $moduleJoinable @@ -255,10 +283,11 @@ foreach ($module in $modules | Sort-Object Name) { $totalMismatches += $moduleProblems.Count $totalDispatchers += $moduleDispatchers $totalUnparseable += $moduleUnparseable + $totalCorrected += $moduleCorrected } Write-Host '' -Write-Host "TOTAL: $totalMatched of $totalJoinable cmdlets match the oracle across $($modules.Count) module(s) (+$totalDispatchers dispatcher cmdlet(s) skipped, +$totalUnparseable cast cmdlet(s) skipped)." +Write-Host "TOTAL: $totalMatched of $totalJoinable cmdlets match the oracle across $($modules.Count) module(s) (+$totalDispatchers dispatcher cmdlet(s) skipped, +$totalUnparseable cast cmdlet(s) skipped, +$totalCorrected deliberately corrected)." if ($totalMismatches -gt 0) { exit 1 diff --git a/tools/WrapperGenerator.Tests/NamingTests.cs b/tools/WrapperGenerator.Tests/NamingTests.cs index 902a4568a19..66c84ca38c7 100644 --- a/tools/WrapperGenerator.Tests/NamingTests.cs +++ b/tools/WrapperGenerator.Tests/NamingTests.cs @@ -24,6 +24,11 @@ public sealed class SingularizerTests [InlineData("Access", "Access")] [InlineData("Status", "Status")] [InlineData("Analysis", "Analysis")] + // "Whois" also hits the is-guard — a deliberate correction, not a parity pin: the SDK + // ships Get-MgSecurityThreatIntelligenceHostWhoi (AutoRest inflected the trailing + // "whois" segment) while its 28 whoisRecords/whoisHistoryRecords siblings keep "Whois". + // See edge-cases/naming-edge-cases.md. + [InlineData("Whois", "Whois")] // plain s [InlineData("Messages", "Message")] [InlineData("Plans", "Plan")] @@ -36,10 +41,12 @@ public sealed class SingularizerTests [InlineData("Cookies", "Cookie")] [InlineData("Skus", "Sku")] // invariants (Get-MgUserSettingWindows, Get-MgDomainVerificationDnsRecord, - // Get-MgDeviceAppManagementIosManagedAppProtection) + // Get-MgDeviceAppManagementIosManagedAppProtection, + // Get-MgSecurityCaseEdiscoveryCaseSearchLastEstimateStatisticsOperation) [InlineData("Windows", "Windows")] [InlineData("Dns", "Dns")] [InlineData("Ios", "Ios")] + [InlineData("Statistics", "Statistics")] // acronyms are never plural forms [InlineData("OS", "OS")] public void SingularizesWords(string word, string expected) @@ -60,6 +67,8 @@ public void SingularizesWords(string word, string expected) [InlineData("OnPremisesSynchronization", "OnPremiseSynchronization")] // version tag: Get-MgSecurityAlertV2 [InlineData("Alerts_v2", "AlertV2")] + // interior "Whois" survives per-word inflection (Get-MgSecurityThreatIntelligenceWhoisHistoryRecord) + [InlineData("WhoisHistoryRecords", "WhoisHistoryRecord")] public void SingularizesSegments(string segment, string expected) { Assert.Equal(expected, Singularizer.SingularizeSegment(segment)); @@ -86,6 +95,9 @@ private static CmdletNaming Resolve(string method, string path) => [InlineData("GET", "/identity/conditionalAccess/policies/{conditionalAccessPolicy-id}", "Get", "MgIdentityConditionalAccessPolicy")] [InlineData("GET", "/planner/plans", "Get", "MgPlannerPlan")] [InlineData("GET", "/security/alerts_v2", "Get", "MgSecurityAlertV2")] + [InlineData("GET", "/security/threatIntelligence/whoisRecords/{whoisRecord-id}", "Get", "MgSecurityThreatIntelligenceWhoisRecord")] + // interior "Statistics" survives per-word inflection (invariant found via the DEVX API's Humanizer exception list) + [InlineData("GET", "/security/cases/ediscoveryCases/{ediscoveryCase-id}/searches/{ediscoverySearch-id}/lastEstimateStatisticsOperation", "Get", "MgSecurityCaseEdiscoveryCaseSearchLastEstimateStatisticsOperation")] [InlineData("PATCH", "/admin/reportSettings", "Update", "MgAdminReportSetting")] [InlineData("GET", "/schemaExtensions", "Get", "MgSchemaExtension")] [InlineData("GET", "/domains/{domain-id}", "Get", "MgDomain")] @@ -109,6 +121,22 @@ public void ResolvesPublishedSdkNames(string method, string path, string expecte Assert.Equal($"{expectedVerb}{expectedNoun}Command", naming.ClassName); } + [Theory] + // Deliberate corrections: the published name is wrong (an AutoRest naming defect) and the + // generator emits the corrected name instead of reproducing it. Every entry here must have + // an edge-cases/naming-edge-cases.md entry and a matching row in + // Compare-WrapperCmdletNames.ps1's $deliberateCorrections table, so the parity gate + // reports it as [CORRECTED], not a failure. + // Shipped: Get-MgSecurityThreatIntelligenceHostWhoi — the only whois-family cmdlet (of 30) + // where "Whois" was inflected to "Whoi". + [InlineData("GET", "/security/threatIntelligence/hosts/{host-id}/whois", "Get", "MgSecurityThreatIntelligenceHostWhois")] + public void AppliesDeliberateNameCorrections(string method, string path, string expectedVerb, string expectedNoun) + { + var naming = Resolve(method, path); + Assert.Equal(expectedVerb, naming.VerbName); + Assert.Equal(expectedNoun, naming.Noun); + } + [Theory] // The builder expression is the Kiota request-builder chain the emitted cmdlet calls // (client..GetAsync()). A property per fixed segment, an indexer per path parameter. diff --git a/tools/WrapperGenerator/README.md b/tools/WrapperGenerator/README.md index 185f50236f4..8943b4bb30c 100644 --- a/tools/WrapperGenerator/README.md +++ b/tools/WrapperGenerator/README.md @@ -6,7 +6,7 @@ Generates the PowerShell **cmdlets** for the Microsoft Graph SDK from Graph's Op The Microsoft Graph PowerShell SDK is thousands of cmdlets, and customers have scripts that depend on their exact names — `Get-MgUserMessage`, not `Get-MgUsersMessages`. Those names follow conventions, but the conventions are fiddly (singular nouns, a `Mg` prefix, a handful of hand-tuned exceptions), and the SDK's current generator (AutoRest) has quietly dropped cmdlets when names collided. -This tool regenerates those cmdlets from the same OpenAPI description **while reproducing the published names exactly**, so a regenerated module is a drop-in replacement. Because name parity is the hard part, most of the tool is a naming engine; the rest is a straightforward C# code emitter. +This tool regenerates those cmdlets from the same OpenAPI description **while reproducing the published names exactly**, so a regenerated module is a drop-in replacement. Because name parity is the hard part, most of the tool is a naming engine; the rest is a straightforward C# code emitter. The one exception to "exactly": a handful of published names are AutoRest naming defects (e.g. `Get-MgSecurityThreatIntelligenceHostWhoi`, where "Whois" lost its `s`) that the generator deliberately corrects — each is pinned by a test, allowlisted in the parity gate, and documented in the edge-case catalog ([edge-cases/naming-edge-cases.md](edge-cases/naming-edge-cases.md), one file per class of issue). ## What it produces @@ -155,16 +155,16 @@ dotnet run --project tools/WrapperGenerator -- ` **Test** — two layers: ```powershell -# 1. Naming rules pinned to published Microsoft.Graph names (69 tests) +# 1. Naming rules pinned to published Microsoft.Graph names dotnet test tools/WrapperGenerator.Tests -# => Passed! - Failed: 0, Passed: 82, Total: 82 +# => Passed! - Failed: 0, Passed: 88, Total: 88 # 2. Parity gate: generate, then check every cmdlet name against Graph's own command inventory .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath # => Mail [v1.0]: 4 of 4 cmdlets match the oracle ... EXIT CODE: 0 ``` -The unit tests guard the naming rules (their expected values are real published names from `src/Authentication/Authentication/custom/common/MgCommandMetadata.json`). The parity gate checks actual generated output against that same inventory. There is **no** test yet that the generated cmdlets *compile* — that needs step 1's client to compile against. +The unit tests guard the naming rules (their expected values are real published names from `src/Authentication/Authentication/custom/common/MgCommandMetadata.json`). The parity gate checks actual generated output against that same inventory; names on the deliberate-corrections list ([edge-cases/naming-edge-cases.md](edge-cases/naming-edge-cases.md)) are reported as `[CORRECTED]` instead of failing. There is **no** test yet that the generated cmdlets *compile* — that needs step 1's client to compile against. ## Gaps / not done yet diff --git a/tools/WrapperGenerator/Singularizer.cs b/tools/WrapperGenerator/Singularizer.cs index ab67a0aa412..05b0710b86e 100644 --- a/tools/WrapperGenerator/Singularizer.cs +++ b/tools/WrapperGenerator/Singularizer.cs @@ -31,12 +31,15 @@ public static partial class Singularizer // Words that end in "s" but are not plurals. The SDK keeps them as-is: // /users/{id}/settings/windows ships as Get-MgUserSettingWindows, verificationDnsRecords // as Get-MgDomainVerificationDnsRecord, iosManagedAppProtections as - // Get-MgDeviceAppManagementIosManagedAppProtection. + // Get-MgDeviceAppManagementIosManagedAppProtection, lastEstimateStatisticsOperation as + // Get-MgSecurityCaseEdiscoveryCaseSearchLastEstimateStatisticsOperation ("Statistics" is + // also on the DEVX API's Humanizer exception list in PowershellFormatter.cs). private static readonly HashSet Invariants = new(StringComparer.Ordinal) { "Windows", "Dns", "Ios", + "Statistics", }; // Splits Pascal or camel text into words. Handles acronym runs ("OS" in "MacOSDmgApp"), @@ -93,7 +96,8 @@ public static string SingularizeWord(string word) if (EndsWithSibilantEs(word)) return word[..^2]; // Businesses -> Business, Mailboxes -> Mailbox if (word.EndsWith("ss", StringComparison.Ordinal) || word.EndsWith("us", StringComparison.Ordinal) || word.EndsWith("is", StringComparison.Ordinal)) - return word; // Access, Status, Analysis stay put + return word; // Access, Status, Analysis stay put; keeping "Whois" is a deliberate + // fix of shipped ...HostWhoi (edge-cases/naming-edge-cases.md) if (word.EndsWith('s')) return word[..^1]; // Messages -> Message, Plans -> Plan return word; diff --git a/tools/WrapperGenerator/edge-cases/naming-edge-cases.md b/tools/WrapperGenerator/edge-cases/naming-edge-cases.md new file mode 100644 index 00000000000..0c70d0a06e6 --- /dev/null +++ b/tools/WrapperGenerator/edge-cases/naming-edge-cases.md @@ -0,0 +1,138 @@ +# Naming edge cases + +This folder is the wrapper generator's edge-case catalog: one Markdown file per **class** of +issue, each entry written with the same fixed fields so the files stay cheap to maintain and +trivial to convert to JSON for automated processing. This file covers the first class: +**cmdlet-naming defects** — cases where the published Microsoft.Graph name is an artifact of +the previous generator (AutoRest) rather than the name the conventions would produce. + +Two policies govern the entries (agreed 2026-08-03/04, wrapper-generator review + sync): + +- **Obviously wrong published names are corrected, not reproduced.** The shipped SDK is the + baseline, not 100% ground truth. Each correction is a deliberate, documented break from + parity. +- **Corrected names ship without a back-compat alias for the old name.** Documenting the + change here and in the migration guide is the agreed mechanism; the generator does not emit + the wrong name in any form. + +## How to add an entry + +A correction lands as four pieces together: + +1. **Fix** — the naming rule change (or, as with Whois, confirmation that the existing rules + already produce the correct name). +2. **Pinned test** — a row in `AppliesDeliberateNameCorrections` (NamingTests.cs) so the + corrected name cannot regress silently. Parity-preserving edge cases go in the regular + pinned tests instead. +3. **Gate entry** — a row in `$deliberateCorrections` in `tools/Compare-WrapperCmdletNames.ps1` + mapping the shipped name to the corrected one, so the parity gate reports `[CORRECTED]` + instead of failing. +4. **Catalog entry** — a section below using the fixed field template. + +Entry template (keep the field names exact so the file converts cleanly): + +``` +## +- **Class:** +- **Status:** +- **Evidence:** +- **Decision:** +- **Migration impact:** +- **References:** +``` + +## Status summary + +| Case | Class | Status | +|---|---|---| +| `HostWhoi` → `HostWhois` | inflection-defect | corrected | +| operationId preposition truncation | operationid-truncation | structurally-avoided | +| `SkypeForBusiness` subject truncation | operationid-truncation | not-yet-reachable | +| `Cookies`/`Skus`/`Dns`/`Ios`/`Statistics` quirks | inflection-defect | reproduced-for-parity | + +## Whois truncated to Whoi on the host navigation + +- **Class:** inflection-defect +- **Status:** corrected +- **Evidence:** `GET /security/threatIntelligence/hosts/{host-id}/whois` shipped as + `Get-MgSecurityThreatIntelligenceHostWhoi` (v1.0 and beta): AutoRest's inflector treated the + trailing `whois` segment as a plural and stripped the `s`. The shipped SDK is inconsistent + with itself — the other 28 whois-family commands in MgCommandMetadata.json + (`.../whoisRecords`, `.../whoisHistoryRecords`, and their children) all keep **Whois** + intact, e.g. `Get-MgSecurityThreatIntelligenceWhoisRecord`. +- **Decision:** emit `Get-MgSecurityThreatIntelligenceHostWhois` / `Get-MgBetaSecurityThreatIntelligenceHostWhois`. + The singularizer's `is`-guard (the rule that keeps Access/Status/Analysis) already produces + `Whois`, so no rule change was needed — the corrected behavior is pinned rather than coded. +- **Migration impact:** scripts calling `Get-MgSecurityThreatIntelligenceHostWhoi` must add the + trailing `s`; no alias is emitted for the old name. Belongs in the migration guide when the + Security module is generated for real. +- **References:** pinned in `AppliesDeliberateNameCorrections` (NamingTests.cs); gate rows in + `$deliberateCorrections` (Compare-WrapperCmdletNames.ps1). + +## operationId preposition/linking-verb truncation + +- **Class:** operationid-truncation +- **Status:** structurally-avoided +- **Evidence:** AutoRest built cmdlet names from **operationIds** and truncated them at + prepositions and linking verbs, so ids like `...ByRef...` lost everything after the + preposition. The SDK worked around it with hand-written rename directives per affected + command. +- **Decision:** no mitigation needed for path-derived nouns — this generator never reads the + operationId; nouns come from URL path segments (CmdletNaming.cs), so the defect class cannot + occur there. Two watch items: (a) **OData actions/functions** (not yet generated) take their + names from an operationId-like segment (`microsoft.graph.assignLicense`, + `getSkypeForBusiness...`) — when that support lands, word-splitting must not treat + prepositions as truncation points; (b) path segments that legitimately contain prepositions + (`termsAndConditions`) are already pinned — the singularizer inflects per word and keeps the + `And` (`TermAndCondition`). +- **Migration impact:** none today. +- **References:** issue [microsoftgraph/msgraph-sdk-powershell#912](https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/912), + PR [#915](https://github.com/microsoftgraph/msgraph-sdk-powershell/pull/915). + +## SkypeForBusiness subject names + +- **Class:** operationid-truncation +- **Status:** not-yet-reachable +- **Evidence:** historically AutoRest truncated subjects containing `SkypeForBusiness` at the + `For`. The shipped names are correct today + (`Get-MgReportSkypeForBusinessActivityUserDetail`, etc.), so there is nothing to correct — + but every affected endpoint is an OData function + (`/reports/getSkypeForBusinessActivityCounts(period='{period}')`), a shape this generator + does not emit yet. +- **Decision:** when function support is implemented, add pinned tests for the + `SkypeForBusiness` family so the `For` survives word-splitting. +- **Migration impact:** none. +- **References:** [Azure/autorest.powershell#795](https://github.com/Azure/autorest.powershell/issues/795). + +## Inflection quirks reproduced for parity + +- **Class:** inflection-defect +- **Status:** reproduced-for-parity +- **Evidence:** auditing every v1.0 GET in MgCommandMetadata.json against the singularizer + surfaced four words where shipped names disagree with naive inflection rules: `Cookies` → + `Cookie` (not `Cooky`), `Skus` → `Sku` (despite the `us`-guard), and `Dns`/`Ios` kept as-is. + A fifth, `Statistics`, came from cross-checking the DEVX API's Humanizer exception list: + the shipped SDK keeps it intact everywhere, including as an interior word + (`Get-MgSecurityCaseEdiscoveryCaseSearchLastEstimateStatisticsOperation`, + `Get-MgBetaUserActivityStatistics`), where the plain s-drop rule would have produced + `Statistic`. +- **Decision:** these shipped names are *reasonable*, just not what naive rules produce, so the + generator reproduces them via the irregulars/invariants tables in Singularizer.cs. The + README's rule table cites the proving cmdlet for each. +- **Migration impact:** none — these are parity-preserving. +- **References:** commit `a429b5999c`; Singularizer.cs `Irregulars`/`Invariants`; the DEVX + API's Humanizer vocabulary in `OpenAPIService/PowershellFormatter.cs` (private + `microsoftgraph/microsoft-graph-devx-api` repo) — its five entries are `drives→drive`, + `data`, `delta`, `quota` (Humanizer-specific mistakes this rule engine never makes) and + `statistics` (the one that applied here). + +## Watch list + +Cases spotted but deliberately not acted on yet, so they aren't lost: + +- **`usageRights` vs `rights` (beta-only):** the shipped SDK keeps `usageRights` plural + (`Get-MgBetaDeviceUsageRights` for `/devices/{id}/usageRights`) but singularizes bare + `rights` (`Get-MgBetaGroupSiteInformationProtectionSensitivityLabelRight` for + `.../sensitivityLabels/{id}/rights`). Our rules match the bare-`rights` case and would + diverge on `usageRights`. All affected paths are beta; resolve when the beta parity audit + runs. From 695cfe74e1454598fe503662c8fea5406fce3f1e Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Thu, 6 Aug 2026 11:48:19 -0700 Subject: [PATCH 04/13] feat(wrapper-generator): add module packaging and smoke-test scripts Build-WrapperModule.ps1 turns one OpenAPI doc into an importable module (kiota client + wrappers + csproj + dll + PSD1 manifest), reading the Kiota-compatible docs by default with a hard kiota timeout and per-module doc fallback. Test-WrapperModule.ps1 imports each build in a fresh pwsh and verifies exports, worker pairing, and the sessionless NoGraphSession path. All 35 cmdlet-producing v1.0 modules build and pass. --- tools/Build-WrapperModule.ps1 | 228 ++++++++++++++++++++++++++++++++++ tools/Test-WrapperModule.ps1 | 145 +++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 tools/Build-WrapperModule.ps1 create mode 100644 tools/Test-WrapperModule.ps1 diff --git a/tools/Build-WrapperModule.ps1 b/tools/Build-WrapperModule.ps1 new file mode 100644 index 00000000000..dc67dc464c8 --- /dev/null +++ b/tools/Build-WrapperModule.ps1 @@ -0,0 +1,228 @@ +<# +.SYNOPSIS +Builds installable wrapper modules end to end: Kiota client + generated cmdlets + compiled +dll + module manifest. + +.DESCRIPTION +For each module name, reproduces the pipeline the Mail spike proved: + + 1. kiota generate -> //src/Client (ApiClient + models) + 2. WrapperGenerator -> //src/Cmdlets (one *.g.cs per cmdlet) + 3. write csproj -> //src/ + 4. dotnet build -> //src/bin//net10.0/ + 5. New-ModuleManifest -> .psd1 next to the dll + +Both generators consume the SAME OpenAPI document, so the wrappers always match the client +they compile against. + +The module is named Microsoft.Graph.Wrapper. so it imports side by side with an +installed official Microsoft.Graph. without collision. + +The manifest exports EVERY cmdlet, including the internal *_Get/*_List workers: the public +Get-* dispatchers forward to the workers by name via InvokeCommand.InvokeScript, so a +manifest that hides the workers breaks dispatch ("term not recognized"). Worker visibility +needs its own dispatch design and is tracked in the module-wiring issue. + +Everything is written under artifacts/ (gitignored); nothing this script produces is +committed. To check cmdlet-name parity for a built module, point the parity gate at its +cmdlets folder: + .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath artifacts\wrapper-modules\\src\Cmdlets + +.PARAMETER Module +One or more module names, each matching an OpenAPI doc at //.yml +(e.g. Mail, Calendar, Users.Actions). + +.PARAMETER ApiVersion +v1.0 (default) or beta. + +.PARAMETER SpecRoot +Root folder of the OpenAPI docs. Default: /openApiDocs_KiotaCompat — the Kiota-suitable +conversion (style=Plain, discriminators preserved). The PowerShell-profile docs under +openApiDocs flatten types like microsoft.graph.Dictionary into empty schemas, which kiota +rejects (Search, Identity.SignIns, Identity.Governance, ConfigurationManagement) or hangs on +(Sites). A module missing under SpecRoot falls back to /openApiDocs with a warning. + +.PARAMETER OutputRoot +Root folder for the built modules. Default: /artifacts/wrapper-modules. + +.PARAMETER Configuration +dotnet build configuration. Default: Debug. + +.PARAMETER SkipKiota +Reuse the previously generated client (fast inner loop when only the wrappers changed). + +.EXAMPLE +.\tools\Build-WrapperModule.ps1 -Module Mail + +.EXAMPLE +.\tools\Build-WrapperModule.ps1 -Module Mail,Calendar -ApiVersion v1.0 +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string[]]$Module, + [ValidateSet('v1.0', 'beta')] + [string]$ApiVersion = 'v1.0', + [string]$SpecRoot, + [string]$OutputRoot, + [string]$Configuration = 'Debug', + [switch]$SkipKiota +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +if (-not $SpecRoot) { $SpecRoot = Join-Path $repoRoot 'openApiDocs_KiotaCompat' } +if (-not $OutputRoot) { $OutputRoot = Join-Path $repoRoot 'artifacts\wrapper-modules' } +$generatorProject = Join-Path $repoRoot 'tools\WrapperGenerator' +$authCsproj = Join-Path $repoRoot 'src\Authentication\Authentication\Microsoft.Graph.Authentication.csproj' + +if (-not (Get-Command kiota -ErrorAction SilentlyContinue)) { + Write-Error "kiota CLI not found on PATH. Install: dotnet tool install --global Microsoft.OpenApi.Kiota" + exit 1 +} + +# Same extraction the parity gate uses: the emitted [Cmdlet(VerbsX.Verb, "Noun")] attribute +# is the source of truth for what the dll will export, without having to load the assembly. +$cmdletAttrPattern = '\[Cmdlet\(Verbs\w+\.(\w+),\s*"((?:\\.|[^"\\])*)"' +function Get-EmittedCmdletNames { + param([string]$CmdletsDir) + Get-ChildItem -Path $CmdletsDir -Filter '*.g.cs' -File | ForEach-Object { + $match = [regex]::Match((Get-Content -Path $_.FullName -Raw), $cmdletAttrPattern) + if ($match.Success) { + "$($match.Groups[1].Value)-$([regex]::Unescape($match.Groups[2].Value))" + } + } +} + +function Build-OneModule { + param([string]$Name) + + $started = Get-Date + $result = [pscustomobject]@{ + Module = $Name; Status = 'FAILED'; FailedAt = ''; CmdletCount = 0; Psd1 = ''; Seconds = 0; Error = '' + } + + try { + $spec = Join-Path $SpecRoot "$ApiVersion\$Name.yml" + if (-not (Test-Path $spec)) { + $fallback = Join-Path $repoRoot "openApiDocs\$ApiVersion\$Name.yml" + if (Test-Path $fallback) { + Write-Warning "$Name has no doc under $SpecRoot; falling back to $fallback" + $spec = $fallback + } + else { $result.FailedAt = 'spec'; $result.Error = "no OpenAPI doc at $spec"; return $result } + } + + $moduleName = "Microsoft.Graph.Wrapper.$Name" + $clientNs = "Microsoft.Graph.PowerShell.$Name.Client" + $srcDir = Join-Path $OutputRoot "$Name\src" + $clientDir = Join-Path $srcDir 'Client' + $cmdletsDir = Join-Path $srcDir 'Cmdlets' + New-Item -ItemType Directory -Force -Path $srcDir | Out-Null + + if (-not $SkipKiota -or -not (Test-Path (Join-Path $clientDir 'ApiClient.cs'))) { + # Run kiota with a hard timeout: it can hang silently on some specs (v1.0 Sites sat + # idle for 35+ minutes with zero CPU), and a hung child must fail this module, not + # stall the whole fan-out. Successful runs take seconds, so 5 minutes is generous. + $kiotaErrLog = Join-Path $srcDir 'kiota-stderr.log' + $kiotaOutLog = Join-Path $srcDir 'kiota-stdout.log' + $kiotaProc = Start-Process kiota -PassThru -NoNewWindow -RedirectStandardError $kiotaErrLog -RedirectStandardOutput $kiotaOutLog -ArgumentList @( + 'generate', '-l', 'CSharp', '-d', $spec, '-c', 'ApiClient', '-n', $clientNs, + '-o', $clientDir, '--clean-output', '--log-level', 'Warning') + if (-not $kiotaProc.WaitForExit(300000)) { + $kiotaProc.Kill() + $result.FailedAt = 'kiota'; $result.Error = 'timed out after 300s (hung, killed)' + return $result + } + if ($kiotaProc.ExitCode -ne 0) { + $result.FailedAt = 'kiota' + $result.Error = (Get-Content -Path $kiotaErrLog -Tail 3 -ErrorAction SilentlyContinue) -join ' | ' + return $result + } + } + + $wrapperOut = & dotnet run --project $generatorProject -- -d $spec -o $cmdletsDir -n $clientNs 2>&1 + if ($LASTEXITCODE -ne 0) { $result.FailedAt = 'wrapper-generator'; $result.Error = ($wrapperOut | Select-Object -Last 3) -join ' | '; return $result } + + # Generated artifact, machine-local by design (absolute reference into this clone). + $csprojPath = Join-Path $srcDir "$moduleName.csproj" + @" + + + + + net10.0 + latest + enable + enable + $moduleName + + true + `$(NoWarn);CS1591 + + + + + + + + + + + + +"@ | Set-Content -Path $csprojPath -Encoding utf8 + + $buildOut = & dotnet build $csprojPath -c $Configuration --nologo -v minimal 2>&1 + if ($LASTEXITCODE -ne 0) { + $result.FailedAt = 'build' + $result.Error = ($buildOut | Where-Object { $_ -match 'error' } | Select-Object -First 3) -join ' | ' + return $result + } + + $cmdlets = @(Get-EmittedCmdletNames -CmdletsDir $cmdletsDir) + if ($cmdlets.Count -eq 0) { $result.FailedAt = 'manifest'; $result.Error = 'no cmdlets emitted'; return $result } + + $binDir = Join-Path $srcDir "bin\$Configuration\net10.0" + $psd1Path = Join-Path $binDir "$moduleName.psd1" + New-ModuleManifest -Path $psd1Path ` + -RootModule "$moduleName.dll" ` + -ModuleVersion '0.1.0' ` + -Author 'Microsoft Graph' -CompanyName 'Microsoft' ` + -Description "Generated Kiota-based wrapper module for $Name ($ApiVersion). Test build - not for release." ` + -CmdletsToExport $cmdlets ` + -FunctionsToExport @() -AliasesToExport @() -VariablesToExport @() + + $result.Status = 'OK' + $result.CmdletCount = $cmdlets.Count + $result.Psd1 = $psd1Path + return $result + } + catch { + if (-not $result.FailedAt) { $result.FailedAt = 'unexpected' } + $result.Error = $_.Exception.Message + return $result + } + finally { + $result.Seconds = [math]::Round(((Get-Date) - $started).TotalSeconds, 1) + } +} + +$results = foreach ($name in $Module) { + Write-Host "=== $name ===" -ForegroundColor Cyan + $r = Build-OneModule -Name $name + if ($r.Status -eq 'OK') { + Write-Host " OK: $($r.CmdletCount) cmdlets -> $($r.Psd1) ($($r.Seconds)s)" -ForegroundColor Green + } + else { + Write-Host " FAILED at $($r.FailedAt): $($r.Error)" -ForegroundColor Yellow + } + $r +} + +Write-Host '' +$results | Format-Table Module, Status, FailedAt, CmdletCount, Seconds -AutoSize | Out-Host + +if ($results.Status -contains 'FAILED') { exit 1 } +exit 0 diff --git a/tools/Test-WrapperModule.ps1 b/tools/Test-WrapperModule.ps1 new file mode 100644 index 00000000000..7460f4f2c60 --- /dev/null +++ b/tools/Test-WrapperModule.ps1 @@ -0,0 +1,145 @@ +<# +.SYNOPSIS +Smoke-tests built wrapper modules the way a user would: Import-Module, inventory the +cmdlets, exercise a dispatcher without a Graph session. + +.DESCRIPTION +Each module is tested in a CHILD pwsh process — a fresh process per module, because +assemblies cannot be unloaded and Import-Module silently no-ops when a same-name module is +already loaded. Checks, per module: + + 1. Import-Module succeeds - the user's first experience + 2. exported cmdlet count == manifest count - nothing silently dropped at load + 3. no orphan workers - every *_Get/*_List worker has its public + dispatcher exported alongside it + 4. one dispatcher invoked with dummy ids and no Graph session: + PASS = NoGraphSession error (the call flowed dispatcher -> worker -> auth path) + FAIL = CommandNotFound (dispatcher->worker forwarding broken: the manifest + visibility trap) or any other unexpected error id + +Modules with no paired list+item GETs have no dispatcher; check 4 reports n/a for them. + +.PARAMETER Module +One or more module names previously built by Build-WrapperModule.ps1. + +.PARAMETER OutputRoot +Root folder the modules were built into. Default: /artifacts/wrapper-modules. + +.PARAMETER Configuration +Build configuration used. Default: Debug. + +.EXAMPLE +.\tools\Test-WrapperModule.ps1 -Module Mail +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string[]]$Module, + [string]$OutputRoot, + [string]$Configuration = 'Debug' +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +if (-not $OutputRoot) { $OutputRoot = Join-Path $repoRoot 'artifacts\wrapper-modules' } + +function Test-OneModule { + param([string]$Name) + + $moduleName = "Microsoft.Graph.Wrapper.$Name" + $psd1 = Join-Path $OutputRoot "$Name\src\bin\$Configuration\net10.0\$moduleName.psd1" + $result = [pscustomobject]@{ + Module = $Name; Pass = $false; Exported = 0; ManifestCount = 0 + OrphanWorkers = 0; Dispatcher = ''; ErrorId = ''; Detail = '' + } + + if (-not (Test-Path $psd1)) { + $result.Detail = "not built: $psd1 missing (run Build-WrapperModule.ps1 first)" + return $result + } + $result.ManifestCount = (Import-PowerShellDataFile -Path $psd1).CmdletsToExport.Count + + # The child prints exactly one JSON line; everything else it may write is noise. + $inner = @" +`$ErrorActionPreference = 'Stop' +Import-Module '$psd1' +`$cmds = Get-Command -Module '$moduleName' +`$workers = @(`$cmds | Where-Object Name -match '_(Get|List)$') +`$orphans = @(`$workers | Where-Object { `$cmds.Name -notcontains (`$_.Name -replace '_(Get|List)$', '') }) +`$dispatcher = `$cmds | Where-Object { `$_.Name -like 'Get-*' -and `$cmds.Name -contains "`$(`$_.Name)_List" } | Select-Object -First 1 +`$errorId = 'N/A' +if (`$dispatcher) { + `$defaultSet = `$dispatcher.ParameterSets | Where-Object IsDefault | Select-Object -First 1 + `$splat = @{} + foreach (`$p in (`$defaultSet.Parameters | Where-Object { `$_.IsMandatory -and `$_.ParameterType -eq [string] })) { + `$splat[`$p.Name] = 'smoke-test' + } + try { + & `$dispatcher @splat -ErrorAction Stop | Out-Null + `$errorId = 'NO-ERROR' + } + catch { + `$errorId = `$_.FullyQualifiedErrorId + } +} +[pscustomobject]@{ + Exported = `$cmds.Count + OrphanWorkers = `$orphans.Count + Dispatcher = if (`$dispatcher) { `$dispatcher.Name } else { '' } + ErrorId = `$errorId +} | ConvertTo-Json -Compress +"@ + + $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner)) + $output = & pwsh -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1 + if ($LASTEXITCODE -ne 0) { + $result.Detail = "Import-Module failed: $(($output | Select-Object -Last 2) -join ' | ')" + return $result + } + + $json = $output | Where-Object { $_ -match '^\{' } | Select-Object -Last 1 + if (-not $json) { $result.Detail = 'child produced no result'; return $result } + $r = $json | ConvertFrom-Json + + $result.Exported = $r.Exported + $result.OrphanWorkers = $r.OrphanWorkers + $result.Dispatcher = $r.Dispatcher + $result.ErrorId = $r.ErrorId + + if ($r.Exported -ne $result.ManifestCount) { + $result.Detail = "exported $($r.Exported) != manifest $($result.ManifestCount)" + } + elseif ($r.OrphanWorkers -gt 0) { + $result.Detail = "$($r.OrphanWorkers) worker(s) without their dispatcher" + } + elseif ($r.ErrorId -notin @('N/A') -and $r.ErrorId -notlike 'NoGraphSession*') { + $result.Detail = if ($r.ErrorId -like '*CommandNotFound*') { + "dispatcher->worker forwarding broken (manifest visibility trap): $($r.ErrorId)" + } else { + "unexpected error id: $($r.ErrorId)" + } + } + else { + $result.Pass = $true + } + return $result +} + +$results = foreach ($name in $Module) { + Write-Host "=== $name ===" -ForegroundColor Cyan + $r = Test-OneModule -Name $name + if ($r.Pass) { + Write-Host " PASS: $($r.Exported) cmdlets; dispatcher $($r.Dispatcher) -> $($r.ErrorId)" -ForegroundColor Green + } + else { + Write-Host " FAIL: $($r.Detail)" -ForegroundColor Yellow + } + $r +} + +Write-Host '' +$results | Format-Table Module, Pass, Exported, ManifestCount, OrphanWorkers, Dispatcher, ErrorId -AutoSize | Out-Host + +if ($results.Pass -contains $false) { exit 1 } +exit 0 From f2de362c085c41daa79126d76685f7a8a1c5ca52 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Thu, 6 Aug 2026 11:48:28 -0700 Subject: [PATCH 05/13] fix(wrapper-generator): align emitted code with real kiota client output Compiling all v1.0 modules against freshly generated kiota clients surfaced eight alignment defects, each fixed and pinned by a test: dispatchers re-wrapped worker errors (NoGraphSession was lost); body properties colliding with path ids (published convention: -DeviceId1); bare model types colliding with namespaces and BCL types (now fully qualified, mirroring kiota's move-inside and reserved-name renames at root and in sub-namespaces); collection responses resolved from their own $ref; underscore members (riskEventTypes_v2 -> RiskEventTypesV2); $select/$expand emitted only where declared; re-fetch only where a GET exists; media/content endpoints skipped like $value. --- tools/WrapperGenerator.Tests/EmitterTests.cs | 61 ++++++- .../GenerationServiceRegressionTests.cs | 41 +++++ .../SchemaPropertiesTests.cs | 47 +++++ tools/WrapperGenerator/CmdletEmitter.cs | 115 ++++++++----- .../PowerShellWrapperGenerationService.cs | 162 +++++++++++++++--- tools/WrapperGenerator/SchemaProperties.cs | 38 +++- 6 files changed, 394 insertions(+), 70 deletions(-) diff --git a/tools/WrapperGenerator.Tests/EmitterTests.cs b/tools/WrapperGenerator.Tests/EmitterTests.cs index 12a4edf54e2..24e8dc174b9 100644 --- a/tools/WrapperGenerator.Tests/EmitterTests.cs +++ b/tools/WrapperGenerator.Tests/EmitterTests.cs @@ -1,10 +1,69 @@ -using WrapperGenerator; +using System.Collections.Generic; +using System.Net.Http; +using WrapperGenerator; using Xunit; namespace WrapperGenerator.Tests; public sealed class EmitterTests { + // A worker's terminating error (e.g. NoGraphSession) surfaces from InvokeScript as a + // RuntimeException; the dispatcher must rethrow the original ErrorRecord, not re-wrap it + // as its own GraphRequestFailed — otherwise every failure loses its identity and the + // "run Connect-MgGraph" guidance never reaches the user. Found by the module smoke test. + [Fact] + public void DispatcherRethrowsTheWorkersOriginalErrorRecord() + { + var list = Naming.Resolve(new OperationInfo(HttpMethod.Get, "/users/{user-id}/messages")); + var item = Naming.Resolve(new OperationInfo(HttpMethod.Get, "/users/{user-id}/messages/{message-id}")); + + var source = CmdletEmitter.EmitGetDispatcher( + list, item, Naming.WithSuffix(list, "_List"), Naming.WithSuffix(item, "_Get"), + new EmitContext("Test.Client"), "Message", "MessageCollectionResponse", + new HashSet(), new HashSet()); + + Assert.Contains("catch (RuntimeException rex) when (rex.ErrorRecord is not null)", source); + Assert.Contains("ThrowTerminatingError(rex.ErrorRecord);", source); + } + + // A collision-renamed property must emit the suffixed PARAMETER but assign the model's + // real property: -DeviceId1 binds, body.DeviceId receives (Update-MgDevice pattern). + [Fact] + public void EmitsSuffixedParameterButAssignsRealModelProperty() + { + var naming = Naming.Resolve(new OperationInfo(HttpMethod.Patch, "/devices/{device-id}")); + var properties = SchemaProperties.ResolveParameterNameCollisions( + new[] { new CmdletProperty("deviceId", "DeviceId", "string", IsArray: false) }, + naming.PathParamNames); + + var source = CmdletEmitter.EmitUpdate(naming, new EmitContext("Test.Client"), "Device", properties, hasPasswordProfile: false); + + Assert.Contains("public string? DeviceId1 { get; set; }", source); + Assert.Contains("body.DeviceId = DeviceId1;", source); + Assert.Contains("IsParameterBound(nameof(DeviceId1))", source); + } + + // PATCH-only resources (/places/{id}) have no GetAsync on their kiota builder, so the + // 204 re-fetch must be emitted only when the path has a GET (found by compiling the + // Calendar module). Without the re-fetch, a bodiless 204 writes nothing — same as the + // published SDK's Update behavior. + [Fact] + public void UpdateEmitsReFetchOnlyWhenPathHasGet() + { + var naming = Naming.Resolve(new OperationInfo(HttpMethod.Patch, "/places/{place-id}")); + var props = new[] { new CmdletProperty("displayName", "DisplayName", "string", IsArray: false) }; + var ctx = new EmitContext("Test.Client"); + + var withGet = CmdletEmitter.EmitUpdate(naming, ctx, "Place", props, hasPasswordProfile: false, reFetchAfterUpdate: true); + Assert.Contains("re-fetching the updated resource", withGet); + Assert.Contains(".GetAsync()", withGet); + + var withoutGet = CmdletEmitter.EmitUpdate(naming, ctx, "Place", props, hasPasswordProfile: false, reFetchAfterUpdate: false); + Assert.DoesNotContain("re-fetching the updated resource", withoutGet); + Assert.DoesNotContain(".GetAsync()", withoutGet); + Assert.Contains("if (result is not null)", withoutGet); + } + // A spec-derived noun or header name containing a double quote must be escaped where it is // interpolated into a generated C# string literal, or the generated source will not compile. [Fact] diff --git a/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs b/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs index fb5f07a471e..7275265a81c 100644 --- a/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs +++ b/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs @@ -13,6 +13,47 @@ namespace WrapperGenerator.Tests; public sealed class GenerationServiceRegressionTests { + // Kiota moves a model INTO a same-named sub-namespace when both exist: + // microsoft.graph.security (alongside microsoft.graph.security.*) generates as + // Models.Security.Security, and a bare "Security" reference resolves to the namespace + // instead — it does not compile (found by building the Security module end to end). + [Theory] + // always fully qualified: bare "Directory" resolved to System.IO.Directory under + // implicit usings (Identity.DirectoryManagement), bare "Security" to the sub-namespace. + [InlineData("microsoft.graph.user", "Test.Models.User")] + [InlineData("microsoft.graph.directory", "Test.Models.Directory")] + [InlineData("microsoft.graph.security", "Test.Models.Security.Security")] + [InlineData("microsoft.graph.security.alert", "Test.Models.Security.Alert")] + [InlineData("microsoft.graph.partners", "Test.Models.Partners.Partners")] + public void ResolvesModelTypeNamesTheWayKiotaLaysThemOut(string schemaName, string expected) + { + var subNamespaces = new HashSet { "Security", "Partners", "CallRecords" }; + Assert.Equal(expected, + PowerShellWrapperGenerationService.ResolveModelTypeName(schemaName, "Test.Models", subNamespaces)); + } + + // Kiota renames reserved class names (BCL conflicts) by appending "Object", then dedupes + // numerically: microsoft.graph.directory -> DirectoryObject1, because directoryObject + // already exists (verified against the Identity.DirectoryManagement client). + [Fact] + public void AppliesKiotaReservedNameRenames() + { + var renames = new Dictionary + { + ["Directory"] = "DirectoryObject1", + ["IdentityGovernance.Task"] = "TaskObject", + }; + Assert.Equal("Test.Models.DirectoryObject1", + PowerShellWrapperGenerationService.ResolveModelTypeName( + "microsoft.graph.directory", "Test.Models", new HashSet(), renames)); + // Reserved renames apply inside sub-namespaces too: identityGovernance.task + // generates as Models.IdentityGovernance.TaskObject (verified against the + // Identity.Governance client). + Assert.Equal("Test.Models.IdentityGovernance.TaskObject", + PowerShellWrapperGenerationService.ResolveModelTypeName( + "microsoft.graph.identityGovernance.task", "Test.Models", new HashSet { "IdentityGovernance" }, renames)); + } + [Fact] public async Task GenerateAsync_SkipsGetWithoutJsonSuccessSchema_DoesNotThrow() { diff --git a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs index 7ac2b68961a..9d704719812 100644 --- a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs +++ b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs @@ -8,6 +8,53 @@ namespace WrapperGenerator.Tests; public sealed class SchemaPropertiesTests { + // Kiota strips underscores when naming model members: signIn's "riskEventTypes_v2" + // becomes RiskEventTypesV2 (verified against a generated SignIn model). The body + // assignment targets that member, so extraction must produce the same name. + [Fact] + public void MapsUnderscorePropertyNamesTheWayKiotaDoes() + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["riskEventTypes_v2"] = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Items = new OpenApiSchema { Type = JsonSchemaType.String }, + }, + }, + }; + + var property = Assert.Single(SchemaProperties.ExtractPrimitiveProperties(schema)); + Assert.Equal("RiskEventTypesV2", property.PascalName); + Assert.Equal("riskEventTypes_v2", property.OpenApiName); + } + + // PATCH /devices/{device-id} carries a body property "deviceId" (Entra's device + // identifier — a different value from the path's object id). The published SDK ships + // both as -DeviceId and -DeviceId1; the resolver reproduces that "1" suffix. The body + // assignment target (PascalName) must stay untouched — only the parameter renames. + [Fact] + public void SuffixesBodyPropertyThatCollidesWithPathParameter() + { + var properties = new[] + { + new CmdletProperty("deviceId", "DeviceId", "string", IsArray: false), + new CmdletProperty("displayName", "DisplayName", "string", IsArray: false), + }; + + var resolved = SchemaProperties.ResolveParameterNameCollisions(properties, new[] { "DeviceId" }); + + var renamed = Assert.Single(resolved, p => p.OpenApiName == "deviceId"); + Assert.Equal("DeviceId1", renamed.ParameterName); + Assert.Equal("DeviceId", renamed.PascalName); + + var untouched = Assert.Single(resolved, p => p.OpenApiName == "displayName"); + Assert.Equal("DisplayName", untouched.ParameterName); + } + private static OpenApiSchema Scalar(JsonSchemaType type, bool readOnly = false, string? format = null) => new() { Type = type, ReadOnly = readOnly, Format = format }; diff --git a/tools/WrapperGenerator/CmdletEmitter.cs b/tools/WrapperGenerator/CmdletEmitter.cs index 79914e69b18..f88ae8742b0 100644 --- a/tools/WrapperGenerator/CmdletEmitter.cs +++ b/tools/WrapperGenerator/CmdletEmitter.cs @@ -154,10 +154,21 @@ private static string EmitCallWithOptionalHeaders(CmdletNaming naming, string me return $"{call}{args}requestConfiguration =>\n {{{bindings}\n }})"; } - public static string EmitItemGet(CmdletNaming naming, EmitContext ctx, string entityType) + public static string EmitItemGet(CmdletNaming naming, EmitContext ctx, string entityType, IReadOnlySet queryParamNames) { ArgumentNullException.ThrowIfNull(naming); ArgumentNullException.ThrowIfNull(ctx); + ArgumentNullException.ThrowIfNull(queryParamNames); + + // Only what the operation declares: kiota omits query-parameter properties the doc + // doesn't declare (subscribedSkus/{id} has $select but no $expand), so an + // unconditional binding would not compile against the builder. + var applicable = CollectionQueryOptions + .Where(o => o.ODataName is "$select" or "$expand" && queryParamNames.Contains(o.ODataName)) + .ToList(); + var queryParamDecls = string.Join("\n", applicable.Select(o => o.ParamDecl(null))); + var queryBindings = string.Join("\n\n", applicable.Select(o => o.Binding)); + return $$""" #nullable enable @@ -181,13 +192,7 @@ public class {{naming.ClassName}} : PSCmdlet {{AccessTokenParamDecl()}} - [Parameter(Mandatory = false)] - [Alias("Select")] - public string[]? Property { get; set; } - - [Parameter(Mandatory = false)] - [Alias("Expand")] - public string[]? ExpandProperty { get; set; } +{{queryParamDecls}} {{HeaderParamDecls(naming)}} {{GenericHeadersParamDecl()}} @@ -200,11 +205,7 @@ protected override void ProcessRecord() { result = client.{{naming.BuilderExpression}}.GetAsync(requestConfiguration => { - if (this.IsParameterBound(nameof(Property))) - requestConfiguration.QueryParameters.Select = Property; - - if (this.IsParameterBound(nameof(ExpandProperty))) - requestConfiguration.QueryParameters.Expand = ExpandProperty; +{{queryBindings}} {{HeaderBindings(naming)}} {{GenericHeadersBinding()}} }).GetAwaiter().GetResult(); @@ -315,7 +316,7 @@ protected override void ProcessRecord() } // The dispatcher's list-only parameter declarations: CollectionQueryOptions minus - // $select/$expand, which are shared with the "Get" set and declared once at class level. + // $select/$expand, which are declared separately per the sets that support them. // Declarations only; binding happens in the internal list cmdlet the dispatcher calls. private static IEnumerable<(string ODataName, string ParamDecl)> ListOnlyQueryOptionsForMerge() => CollectionQueryOptions @@ -353,21 +354,38 @@ private static string PairedPathParams(IReadOnlyList sharedNames, IReadO // call shares the caller's session, including an active Connect-MgGraph. public static string EmitGetDispatcher(CmdletNaming listNaming, CmdletNaming itemNaming, CmdletNaming internalListNaming, CmdletNaming internalItemNaming, EmitContext ctx, - string entityType, string collectionResponseType, IReadOnlySet queryParamNames) + string entityType, string collectionResponseType, IReadOnlySet listQueryParamNames, IReadOnlySet itemQueryParamNames) { ArgumentNullException.ThrowIfNull(listNaming); ArgumentNullException.ThrowIfNull(itemNaming); ArgumentNullException.ThrowIfNull(internalListNaming); ArgumentNullException.ThrowIfNull(internalItemNaming); ArgumentNullException.ThrowIfNull(ctx); - ArgumentNullException.ThrowIfNull(queryParamNames); + ArgumentNullException.ThrowIfNull(listQueryParamNames); + ArgumentNullException.ThrowIfNull(itemQueryParamNames); var sharedPathParams = listNaming.PathParamNames; var getOnlyPathParams = itemNaming.PathParamNames.Skip(sharedPathParams.Count).ToList(); - var applicable = ListOnlyQueryOptionsForMerge().Where(o => queryParamNames.Contains(o.ODataName)).ToList(); + var applicable = ListOnlyQueryOptionsForMerge().Where(o => listQueryParamNames.Contains(o.ODataName)).ToList(); var listOnlyParamDecls = string.Join("\n\n", applicable.Select(o => o.ParamDecl)); + // $select/$expand support can differ between the two operations (subscribedSkus + // declares $expand on the list but not the item), so each declaration is scoped to + // the parameter set(s) whose worker actually binds it. + var selectExpandDecls = string.Join("\n\n", new[] { "$select", "$expand" } + .Select(od => + { + var row = CollectionQueryOptions.First(o => o.ODataName == od); + var inList = listQueryParamNames.Contains(od); + var inItem = itemQueryParamNames.Contains(od); + return inList && inItem ? row.ParamDecl(null) + : inList ? row.ParamDecl("List") + : inItem ? row.ParamDecl("Get") + : null; + }) + .Where(static d => d is not null)); + var (sharedHeaders, listOnlyHeaders, getOnlyHeaders) = PartitionHeaderParams(listNaming, itemNaming); var internalListCmdletName = $"{internalListNaming.VerbName}-{internalListNaming.Noun}"; @@ -392,13 +410,7 @@ public class {{listNaming.ClassName}} : PSCmdlet {{AccessTokenParamDecl()}} - [Parameter(Mandatory = false)] - [Alias("Select")] - public string[]? Property { get; set; } - - [Parameter(Mandatory = false)] - [Alias("Expand")] - public string[]? ExpandProperty { get; set; } +{{selectExpandDecls}} {{listOnlyParamDecls}} {{HeaderParamDeclsFor(sharedHeaders, parameterSetName: null)}} @@ -420,6 +432,16 @@ protected override void ProcessRecord() null, MyInvocation.BoundParameters, internalCmdletName); } + // The workers signal failure via ThrowTerminatingError, which InvokeScript surfaces + // as a RuntimeException carrying the worker's ErrorRecord. Rethrow that record + // unchanged so the caller sees the worker's error identity (NoGraphSession, + // GraphRequestFailed, ...) instead of every failure collapsing into a generic + // dispatcher error. + catch (RuntimeException rex) when (rex.ErrorRecord is not null) + { + ThrowTerminatingError(rex.ErrorRecord); + return; + } {{CatchBlock($"ParameterSetName == \"Get\" ? {TargetId(itemNaming)} : {TargetId(listNaming)}")}} } } @@ -485,7 +507,7 @@ protected override void ProcessRecord() """; } - public static string EmitUpdate(CmdletNaming naming, EmitContext ctx, string entityType, IReadOnlyList properties, bool hasPasswordProfile) + public static string EmitUpdate(CmdletNaming naming, EmitContext ctx, string entityType, IReadOnlyList properties, bool hasPasswordProfile, bool reFetchAfterUpdate = true) { ArgumentNullException.ThrowIfNull(naming); ArgumentNullException.ThrowIfNull(ctx); @@ -534,20 +556,9 @@ protected override void ProcessRecord() } {{CatchBlock(TargetId(naming))}} - // Graph often answers a successful PATCH with 204 and no body (seen live on - // schemaExtension update). Re-fetch so the cmdlet returns the updated resource - // instead of nothing. - if (result is null) - { - WriteVerbose("[MgPoC] PATCH succeeded with no response body, re-fetching the updated resource."); - try - { - result = client.{{naming.BuilderExpression}}.GetAsync().GetAwaiter().GetResult(); - } -{{CatchBlock(TargetId(naming), " ")}} - } - - WriteObject(result); +{{(reFetchAfterUpdate ? ReFetchBlock(naming) : "")}} + if (result is not null) + WriteObject(result); } } } @@ -648,18 +659,38 @@ public Task AuthenticateRequestAsync(RequestInformation request, Dictionary $$""" + + if (result is null) + { + WriteVerbose("[MgPoC] PATCH succeeded with no response body, re-fetching the updated resource."); + try + { + result = client.{{naming.BuilderExpression}}.GetAsync().GetAwaiter().GetResult(); + } +{{CatchBlock(TargetId(naming), " ")}} + } +"""; + + // ParameterName (not PascalName) names the parameter: it carries the "1" suffix when a + // body property collides with a path id. The body assignment keeps PascalName — the + // Kiota model property is unaffected by the parameter rename. private static string EmitPropertyParameters(IReadOnlyList properties) => string.Join("\n", properties.Select(p => $$""" [Parameter(Mandatory = false)] - public {{p.PsTypeName}}? {{p.PascalName}} { get; set; } + public {{p.PsTypeName}}? {{p.ParameterName}} { get; set; } """)); private static string EmitPropertyAssignments(IReadOnlyList properties) => string.Join("\n", properties.Select(p => $$""" - if (this.IsParameterBound(nameof({{p.PascalName}}))) - body.{{p.PascalName}} = {{(p.IsArray ? $"{p.PascalName}!.ToList()" : p.PascalName)}}; + if (this.IsParameterBound(nameof({{p.ParameterName}}))) + body.{{p.PascalName}} = {{(p.IsArray ? $"{p.ParameterName}!.ToList()" : p.ParameterName)}}; """)); private static string EmitPasswordProfileParameters() => """ diff --git a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs index 279470cd9a5..f339190253b 100644 --- a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs +++ b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs @@ -19,6 +19,8 @@ public sealed partial class PowerShellWrapperGenerationService private readonly OpenApiDocument document; private readonly GeneratorConfig config; private readonly ILogger logger; + private readonly HashSet modelSubNamespaces; + private readonly Dictionary kiotaReservedRenames; public PowerShellWrapperGenerationService(OpenApiDocument document, GeneratorConfig configuration, ILogger logger) { @@ -28,8 +30,57 @@ public PowerShellWrapperGenerationService(OpenApiDocument document, GeneratorCon this.document = document; config = configuration; this.logger = logger; + + // Kiota nests each dotted schema-name segment as a sub-namespace under Models + // ("security.alert" -> Models.Security.Alert), and when a model's own name matches + // such a namespace ("microsoft.graph.security" alongside "microsoft.graph.security.*") + // it moves the class INSIDE it: Models.Security.Security. Collect those namespace + // roots so ResolveModelTypeName can mirror the move — a bare "Security" would + // otherwise resolve to the namespace, not the type, and fail to compile. + // Model names grouped by the sub-namespace kiota puts them in ("" = Models root): + // needed both for the namespace-move rule and to dedupe reserved-name renames the + // way kiota does (against siblings in the same namespace). + modelSubNamespaces = new HashSet(StringComparer.Ordinal); + var namesByNamespace = new Dictionary>(StringComparer.Ordinal) { [""] = new(StringComparer.Ordinal) }; + foreach (var key in document.Components?.Schemas?.Keys ?? Enumerable.Empty()) + { + var segments = StripGraphPrefix(key).Split('.') + .Select(static s => char.ToUpperInvariant(s[0]) + s[1..]).ToArray(); + if (segments.Length > 1) + modelSubNamespaces.Add(segments[0]); + var ns = string.Join('.', segments[..^1]); + if (!namesByNamespace.TryGetValue(ns, out var names)) + namesByNamespace[ns] = names = new HashSet(StringComparer.Ordinal); + names.Add(segments[^1]); + } + + // Kiota renames model classes whose name is on its C# reserved list (BCL conflicts: + // Directory, File, Task, ...) by appending "Object", then dedupes numerically against + // sibling models. Observed and verified: microsoft.graph.directory generates as + // DirectoryObject1 (directoryObject already exists at the root) and + // microsoft.graph.identityGovernance.task as IdentityGovernance.TaskObject. This + // mirrors observed kiota 1.32.2 behavior — a wrong prediction fails the module + // compile, it cannot fail silently. Keyed by the full Pascal segment path. + kiotaReservedRenames = new Dictionary(StringComparer.Ordinal); + foreach (var (ns, names) in namesByNamespace) + { + foreach (var reserved in KiotaReservedModelNames) + { + if (!names.Contains(reserved)) + continue; + var renamed = reserved + "Object"; + while (names.Contains(renamed)) + renamed += "1"; + kiotaReservedRenames[ns.Length == 0 ? reserved : $"{ns}.{reserved}"] = renamed; + } + } } + // Kiota's C# refiner reserves type names that collide with common BCL types (see + // CSharpReservedClassNamesProvider in microsoft/kiota). Only names observed in Graph + // docs are listed; a new one surfaces as a compile failure in the affected module. + private static readonly string[] KiotaReservedModelNames = ["Directory", "File", "Task", "Type", "Environment"]; + // One GET operation from the first pass, held until we know whether it pairs with a // list/item partner. CollectionValueSchema is the response's "value" array property when // the response is a collection, null for a single entity. It is resolved once here so @@ -92,6 +143,17 @@ public async Task GenerateAsync(CancellationToken cancellationToken) // checks "2XX" first and falls back across 200/201/default and "+json" content // types for the few operations that deviate, rather than doing general content // negotiation the way a generic OpenAPI reader would have to. + // A success response that also declares non-JSON content (octet-stream, + // image/*) is a media download — kiota generates GetAsync returning Stream + // there regardless of any JSON schema the doc also lists (the styled docs + // attach an entity schema to /content endpoints; found by compiling Teams). + // Stream downloads are not generated yet; see the README gap list. + if (httpMethod == HttpMethod.Get && HasNonJsonSuccessContent(operation)) + { + LogSkippedUnsupportedOperation(httpMethod.Method, pathTemplate, "media/stream content endpoint, not generated yet"); + continue; + } + var responseSchema = httpMethod == HttpMethod.Get ? TryGetSuccessJsonSchema(operation) : null; @@ -116,7 +178,8 @@ public async Task GenerateAsync(CancellationToken cancellationToken) { _ when httpMethod == HttpMethod.Delete => CmdletEmitter.EmitRemove(cmdletNaming, ctx), _ when httpMethod == HttpMethod.Post => EmitNewFor(cmdletNaming, ctx, operation), - _ when httpMethod == HttpMethod.Patch => EmitUpdateFor(cmdletNaming, ctx, operation), + _ when httpMethod == HttpMethod.Patch => EmitUpdateFor(cmdletNaming, ctx, operation, + canReFetch: pathItem.Operations?.ContainsKey(HttpMethod.Get) == true), _ => null, }; @@ -177,18 +240,19 @@ private async Task EmitGetOperationsAsync(List getOpera LogSkippedUnsupportedOperation("GET", listOp.Naming.BuilderExpression, "response schema is not a resolvable $ref entity type"); continue; } - var collectionResponseType = listEntityType + "CollectionResponse"; + var collectionResponseType = ResolveCollectionResponseType(listOp.ResponseSchema, ctx.ModelsNamespace, listEntityType); // The two real implementations: separate, independently documented cmdlets, unchanged // from (and reusing) the standalone shapes used for unpaired GETs. var internalListNaming = Naming.WithSuffix(listOp.Naming, "_List"); var internalItemNaming = Naming.WithSuffix(itemOp.Naming, "_Get"); var internalListSource = CmdletEmitter.EmitListGet(internalListNaming, ctx, listEntityType, collectionResponseType, listOp.QueryParams.ToHashSet()); - var internalItemSource = CmdletEmitter.EmitItemGet(internalItemNaming, ctx, entityType); + var internalItemSource = CmdletEmitter.EmitItemGet(internalItemNaming, ctx, entityType, itemOp.QueryParams.ToHashSet()); // The thin public dispatcher on top, presenting the merged Get-MgX surface. var dispatcherSource = CmdletEmitter.EmitGetDispatcher(listOp.Naming, itemOp.Naming, - internalListNaming, internalItemNaming, ctx, entityType, collectionResponseType, listOp.QueryParams.ToHashSet()); + internalListNaming, internalItemNaming, ctx, entityType, collectionResponseType, + listOp.QueryParams.ToHashSet(), itemOp.QueryParams.ToHashSet()); written += await WriteCmdletFileAsync(internalListNaming, internalListSource, cancellationToken).ConfigureAwait(false); written += await WriteCmdletFileAsync(internalItemNaming, internalItemSource, cancellationToken).ConfigureAwait(false); @@ -211,7 +275,8 @@ private async Task EmitGetOperationsAsync(List getOpera continue; } - source = CmdletEmitter.EmitListGet(op.Naming, ctx, listEntityType, listEntityType + "CollectionResponse", op.QueryParams.ToHashSet()); + source = CmdletEmitter.EmitListGet(op.Naming, ctx, listEntityType, + ResolveCollectionResponseType(op.ResponseSchema, ctx.ModelsNamespace, listEntityType), op.QueryParams.ToHashSet()); } else { @@ -221,7 +286,7 @@ private async Task EmitGetOperationsAsync(List getOpera continue; } - source = CmdletEmitter.EmitItemGet(op.Naming, ctx, entityType); + source = CmdletEmitter.EmitItemGet(op.Naming, ctx, entityType, op.QueryParams.ToHashSet()); } written += await WriteCmdletFileAsync(op.Naming, source, cancellationToken).ConfigureAwait(false); @@ -260,7 +325,7 @@ private static bool HasUnsupportedPathSegment(string pathTemplate) => // collectionValueSchema is the already-resolved "value" array property from // GetOperationRecord, so nothing is re-walked here. - private static bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueSchema, string modelsNamespace, out string entityTypeName) + private bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueSchema, string modelsNamespace, out string entityTypeName) { entityTypeName = string.Empty; var itemSchema = collectionValueSchema.Items; @@ -285,7 +350,7 @@ private static bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueS return null; } - private static string? EmitNewFor(CmdletNaming naming, EmitContext ctx, OpenApiOperation operation) + private string? EmitNewFor(CmdletNaming naming, EmitContext ctx, OpenApiOperation operation) { // "application/json" is an intentional, Graph-scoped assumption: Graph request bodies are // JSON, so the content type is indexed directly rather than negotiated. See the matching @@ -295,11 +360,12 @@ private static bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueS return null; if (!TryResolveEntityTypeName(bodySchema, ctx.ModelsNamespace, out var entityType)) return null; - return CmdletEmitter.EmitNew(naming, ctx, entityType, - SchemaProperties.ExtractPrimitiveProperties(bodySchema), SchemaProperties.HasPasswordProfile(bodySchema)); + var properties = SchemaProperties.ResolveParameterNameCollisions( + SchemaProperties.ExtractPrimitiveProperties(bodySchema), naming.PathParamNames); + return CmdletEmitter.EmitNew(naming, ctx, entityType, properties, SchemaProperties.HasPasswordProfile(bodySchema)); } - private static string? EmitUpdateFor(CmdletNaming naming, EmitContext ctx, OpenApiOperation operation) + private string? EmitUpdateFor(CmdletNaming naming, EmitContext ctx, OpenApiOperation operation, bool canReFetch) { // "application/json" is an intentional, Graph-scoped assumption (see EmitNewFor). var bodySchema = TryGetRequestJsonSchema(operation); @@ -307,8 +373,27 @@ private static bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueS return null; if (!TryResolveEntityTypeName(bodySchema, ctx.ModelsNamespace, out var entityType)) return null; - return CmdletEmitter.EmitUpdate(naming, ctx, entityType, - SchemaProperties.ExtractPrimitiveProperties(bodySchema), SchemaProperties.HasPasswordProfile(bodySchema)); + var properties = SchemaProperties.ResolveParameterNameCollisions( + SchemaProperties.ExtractPrimitiveProperties(bodySchema), naming.PathParamNames); + return CmdletEmitter.EmitUpdate(naming, ctx, entityType, properties, SchemaProperties.HasPasswordProfile(bodySchema), canReFetch); + } + + private static bool HasNonJsonSuccessContent(OpenApiOperation operation) + { + if (operation.Responses is null) + return false; + foreach (var key in new[] { "2XX", "200", "201" }) + { + if (!operation.Responses.TryGetValue(key, out var response) || response?.Content is null) + continue; + foreach (var contentType in response.Content.Keys) + { + if (!contentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) + && !contentType.EndsWith("+json", StringComparison.OrdinalIgnoreCase)) + return true; + } + } + return false; } private static IOpenApiSchema? TryGetSuccessJsonSchema(OpenApiOperation operation) @@ -355,27 +440,56 @@ private static bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueS return null; } - private static bool TryResolveEntityTypeName(IOpenApiSchema schema, string modelsNamespace, out string entityTypeName) + private bool TryResolveEntityTypeName(IOpenApiSchema schema, string modelsNamespace, out string entityTypeName) { entityTypeName = string.Empty; var id = schema.GetReferenceId(); if (string.IsNullOrEmpty(id)) return false; - entityTypeName = SchemaNameToTypeName(id, modelsNamespace); + entityTypeName = ResolveModelTypeName(id, modelsNamespace, modelSubNamespaces, kiotaReservedRenames); return true; } - private static string SchemaNameToTypeName(string schemaName, string modelsNamespace) - { - var name = schemaName.StartsWith("microsoft.graph.", StringComparison.Ordinal) + // The collection response type is resolved from the list response's own $ref, not by + // appending "CollectionResponse" to the entity type: kiota's reserved-name rename hits + // the entity but not its collection response (identityGovernance.task -> + // Models.IdentityGovernance.TaskObject, but taskCollectionResponse -> TaskCollectionResponse + // unchanged — found by compiling Identity.Governance). Falls back to the append for + // inline response schemas without a $ref. + private string ResolveCollectionResponseType(IOpenApiSchema listResponseSchema, string modelsNamespace, string listEntityType) => + TryResolveEntityTypeName(listResponseSchema, modelsNamespace, out var fromRef) + ? fromRef + : listEntityType + "CollectionResponse"; + + private static string StripGraphPrefix(string schemaName) => + schemaName.StartsWith("microsoft.graph.", StringComparison.Ordinal) ? schemaName["microsoft.graph.".Length..] : schemaName; - // Kiota nests each dot segment as a sub-namespace under Models ("security.alert" - // becomes Models.Security.Alert). A using directive does not reach into nested - // namespaces, so multi-segment names are fully qualified; single-segment names, - // the common case, stay bare. - var segments = name.Split('.').Select(static segment => char.ToUpperInvariant(segment[0]) + segment[1..]).ToArray(); - return segments.Length == 1 ? segments[0] : $"{modelsNamespace}.{string.Join('.', segments)}"; + // Maps a schema reference id to the C# type name kiota generates for it. Public and pure + // so the mapping rules are directly testable. + // + // Every reference is fully qualified. Bare names break two ways, both found by compiling + // real modules: a name that matches a kiota sub-namespace resolves to the namespace + // instead of the type ("Security"), and a name that matches a BCL type in scope resolves + // to that ("Directory" vs System.IO.Directory under implicit usings). Kiota itself nests + // dotted segments as sub-namespaces ("security.alert" -> Models.Security.Alert), and when + // a model's own name matches such a namespace it moves the class inside it + // (microsoft.graph.security -> Models.Security.Security, verified against a real client). + public static string ResolveModelTypeName(string schemaName, string modelsNamespace, IReadOnlySet modelSubNamespaces, + IReadOnlyDictionary? kiotaReservedRenames = null) + { + ArgumentNullException.ThrowIfNull(schemaName); + ArgumentNullException.ThrowIfNull(modelsNamespace); + ArgumentNullException.ThrowIfNull(modelSubNamespaces); + + var segments = StripGraphPrefix(schemaName).Split('.') + .Select(static segment => char.ToUpperInvariant(segment[0]) + segment[1..]).ToArray(); + if (kiotaReservedRenames is not null && kiotaReservedRenames.TryGetValue(string.Join('.', segments), out var renamed)) + segments[^1] = renamed; + var qualified = $"{modelsNamespace}.{string.Join('.', segments)}"; + return segments.Length == 1 && modelSubNamespaces.Contains(segments[0]) + ? $"{qualified}.{segments[0]}" + : qualified; } } diff --git a/tools/WrapperGenerator/SchemaProperties.cs b/tools/WrapperGenerator/SchemaProperties.cs index 08fe5d4b3e3..30d3ac23023 100644 --- a/tools/WrapperGenerator/SchemaProperties.cs +++ b/tools/WrapperGenerator/SchemaProperties.cs @@ -5,7 +5,12 @@ namespace WrapperGenerator; -public sealed record CmdletProperty(string OpenApiName, string PascalName, string PsTypeName, bool IsArray); +public sealed record CmdletProperty(string OpenApiName, string PascalName, string PsTypeName, bool IsArray) +{ + // The emitted -Parameter name. Differs from PascalName only when the body property + // collides with a path parameter; see ResolveParameterNameCollisions. + public string ParameterName { get; init; } = PascalName; +} // Maps a body schema's top-level primitive properties onto cmdlet parameters. Deliberately // shallow, per team decision: nested complex properties (assignedLicenses, employeeOrgData, @@ -32,11 +37,11 @@ void Walk(IOpenApiSchema s) if (IsPlainScalar(propSchema)) { - result.Add(new CmdletProperty(name, name.ToFirstCharacterUpperCase(), MapPsType(propSchema), IsArray: false)); + result.Add(new CmdletProperty(name, ToKiotaPropertyName(name), MapPsType(propSchema), IsArray: false)); } else if (propSchema.Type == JsonSchemaType.Array && propSchema.Items is { } items && IsPlainScalar(items)) { - result.Add(new CmdletProperty(name, name.ToFirstCharacterUpperCase(), MapPsType(items) + "[]", IsArray: true)); + result.Add(new CmdletProperty(name, ToKiotaPropertyName(name), MapPsType(items) + "[]", IsArray: true)); } } } @@ -45,6 +50,23 @@ void Walk(IOpenApiSchema s) return result; } + // A body property whose Pascal name matches a path parameter would emit a duplicate C# + // property (PATCH /devices/{device-id} has a path id AND a body property "deviceId" — + // different values: the URL takes the object id, the body carries Entra's deviceId). + // The published SDK keeps both reachable by suffixing the body one with "1" + // (Update-MgDevice ships -DeviceId and -DeviceId1); reproduce that convention rather + // than dropping a settable property. + public static IReadOnlyList ResolveParameterNameCollisions( + IReadOnlyList properties, IReadOnlyList pathParamNames) + { + ArgumentNullException.ThrowIfNull(properties); + ArgumentNullException.ThrowIfNull(pathParamNames); + var taken = new HashSet(pathParamNames, StringComparer.Ordinal); + return properties + .Select(p => taken.Contains(p.PascalName) ? p with { ParameterName = p.PascalName + "1" } : p) + .ToList(); + } + // passwordProfile is a nested complex type, so ExtractPrimitiveProperties skips it, but // Graph requires it to create a user. This flag lets the emitter add the two flattened // parameters (-Password, -ForceChangePasswordNextSignIn) that make New-MgUser usable. @@ -88,6 +110,16 @@ public static bool HasPasswordProfile(IOpenApiSchema schema) _ => "string", }; + // Kiota cleans property symbols when generating model members: underscores are dropped + // and the following character upper-cased ("riskEventTypes_v2" -> RiskEventTypesV2, + // verified against a generated SignIn model). The body assignment targets that member, + // so this mapping must match kiota's or the emitted code does not compile. + private static string ToKiotaPropertyName(string openApiName) + { + var parts = openApiName.Split('_', StringSplitOptions.RemoveEmptyEntries); + return string.Concat(parts.Select(static p => char.ToUpperInvariant(p[0]) + p[1..])); + } + // Excludes properties a caller cannot or should not set. "id" is server-assigned. // "@"-prefixed names like "@odata.type" are OData control data that Kiota's serializer // fills in from the model type, and they are not legal C# identifiers anyway. ReadOnly is From 26948e82dedbaab9f7454dc9d5d5d62fb820020d Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Thu, 6 Aug 2026 11:48:55 -0700 Subject: [PATCH 06/13] fix(wrapper-generator): correct PlaceCheckIn names, add Rights invariant, catalog kiota edge cases The full-module parity sweep found two shipped-name issues: AutoRest truncated /places/{id}/checkIns at the preposition (8 commands ship as *-MgPlaceCheck while Get-MgPlaceCheckInCount keeps "In") - corrected per policy with gate rows and pinned tests; and "Rights" needs to be an inflection invariant (Get-MgPrivacySubjectRightsRequest, 42 cmdlets) - Compliance now matches 23 of 23. New edge-cases/kiota-alignment file catalogs the compile-found defect classes; README refreshed. 103 tests. --- tools/Compare-WrapperCmdletNames.ps1 | 10 ++ tools/WrapperGenerator.Tests/NamingTests.cs | 7 ++ tools/WrapperGenerator/README.md | 6 +- tools/WrapperGenerator/Singularizer.cs | 3 + .../edge-cases/kiota-alignment-edge-cases.md | 119 ++++++++++++++++++ .../edge-cases/naming-edge-cases.md | 28 ++++- 6 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 tools/WrapperGenerator/edge-cases/kiota-alignment-edge-cases.md diff --git a/tools/Compare-WrapperCmdletNames.ps1 b/tools/Compare-WrapperCmdletNames.ps1 index e06f24538c6..d1a6b0a5c24 100644 --- a/tools/Compare-WrapperCmdletNames.ps1 +++ b/tools/Compare-WrapperCmdletNames.ps1 @@ -140,6 +140,16 @@ $deliberateCorrections = @{ # cmdlets (whoisRecords, whoisHistoryRecords) all keep "Whois". 'Get-MgSecurityThreatIntelligenceHostWhoi' = 'Get-MgSecurityThreatIntelligenceHostWhois' 'Get-MgBetaSecurityThreatIntelligenceHostWhoi' = 'Get-MgBetaSecurityThreatIntelligenceHostWhois' + # AutoRest truncated /places/{id}/checkIns at the preposition (the #912 defect class): + # shipped ...PlaceCheck, while Get-MgPlaceCheckInCount keeps "In" intact. + 'Get-MgPlaceCheck' = 'Get-MgPlaceCheckIn' + 'New-MgPlaceCheck' = 'New-MgPlaceCheckIn' + 'Update-MgPlaceCheck' = 'Update-MgPlaceCheckIn' + 'Remove-MgPlaceCheck' = 'Remove-MgPlaceCheckIn' + 'Get-MgBetaPlaceCheck' = 'Get-MgBetaPlaceCheckIn' + 'New-MgBetaPlaceCheck' = 'New-MgBetaPlaceCheckIn' + 'Update-MgBetaPlaceCheck' = 'Update-MgBetaPlaceCheckIn' + 'Remove-MgBetaPlaceCheck' = 'Remove-MgBetaPlaceCheckIn' } Write-Host "Loading oracle from $OraclePath ..." diff --git a/tools/WrapperGenerator.Tests/NamingTests.cs b/tools/WrapperGenerator.Tests/NamingTests.cs index 66c84ca38c7..3f101ae7522 100644 --- a/tools/WrapperGenerator.Tests/NamingTests.cs +++ b/tools/WrapperGenerator.Tests/NamingTests.cs @@ -47,6 +47,7 @@ public sealed class SingularizerTests [InlineData("Dns", "Dns")] [InlineData("Ios", "Ios")] [InlineData("Statistics", "Statistics")] + [InlineData("Rights", "Rights")] // acronyms are never plural forms [InlineData("OS", "OS")] public void SingularizesWords(string word, string expected) @@ -98,6 +99,8 @@ private static CmdletNaming Resolve(string method, string path) => [InlineData("GET", "/security/threatIntelligence/whoisRecords/{whoisRecord-id}", "Get", "MgSecurityThreatIntelligenceWhoisRecord")] // interior "Statistics" survives per-word inflection (invariant found via the DEVX API's Humanizer exception list) [InlineData("GET", "/security/cases/ediscoveryCases/{ediscoveryCase-id}/searches/{ediscoverySearch-id}/lastEstimateStatisticsOperation", "Get", "MgSecurityCaseEdiscoveryCaseSearchLastEstimateStatisticsOperation")] + // interior "Rights" survives per-word inflection (Get-MgPrivacySubjectRightsRequest, found by the full-module parity sweep) + [InlineData("GET", "/privacy/subjectRightsRequests/{subjectRightsRequest-id}", "Get", "MgPrivacySubjectRightsRequest")] [InlineData("PATCH", "/admin/reportSettings", "Update", "MgAdminReportSetting")] [InlineData("GET", "/schemaExtensions", "Get", "MgSchemaExtension")] [InlineData("GET", "/domains/{domain-id}", "Get", "MgDomain")] @@ -130,6 +133,10 @@ public void ResolvesPublishedSdkNames(string method, string path, string expecte // Shipped: Get-MgSecurityThreatIntelligenceHostWhoi — the only whois-family cmdlet (of 30) // where "Whois" was inflected to "Whoi". [InlineData("GET", "/security/threatIntelligence/hosts/{host-id}/whois", "Get", "MgSecurityThreatIntelligenceHostWhois")] + // Shipped: New-MgPlaceCheck — AutoRest truncated "CheckIns" at the preposition (#912 + // class) while Get-MgPlaceCheckInCount keeps "In" intact. + [InlineData("GET", "/places/{place-id}/checkIns", "Get", "MgPlaceCheckIn")] + [InlineData("POST", "/places/{place-id}/checkIns", "New", "MgPlaceCheckIn")] public void AppliesDeliberateNameCorrections(string method, string path, string expectedVerb, string expectedNoun) { var naming = Resolve(method, path); diff --git a/tools/WrapperGenerator/README.md b/tools/WrapperGenerator/README.md index 8943b4bb30c..caab1d5e93e 100644 --- a/tools/WrapperGenerator/README.md +++ b/tools/WrapperGenerator/README.md @@ -143,7 +143,7 @@ The wrappers compile and run only alongside step 1's output. Wiring the two into ```powershell dotnet run --project tools/WrapperGenerator -- ` - -d openApiDocs/v1.0/Mail.yml ` + -d openApiDocs_KiotaCompat/v1.0/Mail.yml ` -o ` -n Microsoft.Graph.PowerShell.Mail.Client ` --include-path '/users/{user-id}/message[s]#GET,POST' ` @@ -157,7 +157,7 @@ dotnet run --project tools/WrapperGenerator -- ` ```powershell # 1. Naming rules pinned to published Microsoft.Graph names dotnet test tools/WrapperGenerator.Tests -# => Passed! - Failed: 0, Passed: 88, Total: 88 +# => Passed! - Failed: 0, Passed: 103, Total: 103 # 2. Parity gate: generate, then check every cmdlet name against Graph's own command inventory .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath @@ -168,7 +168,7 @@ The unit tests guard the naming rules (their expected values are real published ## Gaps / not done yet -- **Output isn't wired into a module.** Files go to whatever `-o` folder you pass, in a fixed `MgPoC` namespace. The target design commits wrappers into `src/{Module}/` with a per-module namespace; that alignment (and a namespace override) isn't built. +- **Output isn't committed into `src/{Module}/`.** `tools/Build-WrapperModule.ps1` now turns a module into an importable build under `artifacts/` (kiota client + wrappers + csproj + PSD1; `tools/Test-WrapperModule.ps1` smoke-tests it), with generation reading the Kiota-compatible docs (`openApiDocs_KiotaCompat`) by default. The target design — wrappers committed into `src/{Module}/` with a per-module namespace instead of `MgPoC` — is still open. - **No runtime base classes or real auth flow.** Shared paging, a proper `Connect-MgGraph`/session integration, and base cmdlet classes are a later phase. - **Body binding is shallow** — top-level primitive properties only; no nested/complex types beyond the `passwordProfile` special case. - **Some operation shapes aren't generated** — `$count`/`$ref`/`$value`, delta, OData actions/functions, and cast endpoints. diff --git a/tools/WrapperGenerator/Singularizer.cs b/tools/WrapperGenerator/Singularizer.cs index 05b0710b86e..098a7ff6873 100644 --- a/tools/WrapperGenerator/Singularizer.cs +++ b/tools/WrapperGenerator/Singularizer.cs @@ -40,6 +40,9 @@ public static partial class Singularizer "Dns", "Ios", "Statistics", + // subjectRightsRequests ships keeping "Rights" (Get-MgPrivacySubjectRightsRequest, + // 42 cmdlets across Compliance/Security); usageRights likewise in beta. + "Rights", }; // Splits Pascal or camel text into words. Handles acronym runs ("OS" in "MacOSDmgApp"), diff --git a/tools/WrapperGenerator/edge-cases/kiota-alignment-edge-cases.md b/tools/WrapperGenerator/edge-cases/kiota-alignment-edge-cases.md new file mode 100644 index 00000000000..f7c2775c9c6 --- /dev/null +++ b/tools/WrapperGenerator/edge-cases/kiota-alignment-edge-cases.md @@ -0,0 +1,119 @@ +# Kiota alignment edge cases + +Second class file in the edge-case catalog (see `naming-edge-cases.md` for the catalog +conventions). These cases are places where the wrapper generator's *prediction* of what +kiota generates — type names, builder members, query parameters — met kiota's actual +output and lost. Every entry was found the same way: compiling generated wrappers against +a real kiota client, module by module. That is the point of the packaging pipeline: a wrong +prediction fails a build loudly instead of shipping. + +The systemic backstop for this whole class is the compile gate (tracked with the pipeline +work): these entries document the specific rules learned so far, not a promise that no +others exist. + +## Doc flavor: kiota requires the KiotaCompat conversion + +- **Class:** doc-flavor +- **Status:** handled (Build-WrapperModule.ps1 defaults to openApiDocs_KiotaCompat) +- **Evidence:** the PowerShell-profile docs under `openApiDocs` flatten open types + (`microsoft.graph.Dictionary`, `customExtensionData`, `onAttributeCollectionHandler`) + into empty schemas; kiota rejects them ("the type does not contain any information") in + Search, Identity.SignIns, Identity.Governance, and ConfigurationManagement, and hangs + >35 min on Sites. The `openApiDocs_KiotaCompat` conversion (DEVX API, `style=Plain`, + discriminators preserved) generates all five in seconds. +- **Decision:** KiotaCompat docs are the generator's canonical input. Open question raised + with the team: make them canonical for the whole v3 pipeline. +- **Migration impact:** none — input selection, not output change. +- **References:** tools/DownloadOpenApiDocKiotaCompat.ps1 (provenance); + Build-WrapperModule.ps1 `-SpecRoot`. + +## kiota hangs on specific docs (both flavors) + +- **Class:** doc-flavor +- **Status:** workaround (hard timeout + per-module doc-flavor fallback) +- **Evidence:** kiota 1.32.2 hangs silently (zero CPU, no output) on the *styled* Sites doc + and on the *KiotaCompat* Teams doc — while generating each module fine from the other + flavor. Content-dependent, not size-dependent (larger docs complete in seconds). +- **Decision:** Build-WrapperModule.ps1 kills kiota after 300s and fails the module rather + than stalling a fan-out; Teams builds from the styled doc via `-SpecRoot`. Candidate for + an upstream kiota report once a minimal repro is extracted. +- **Migration impact:** none. + +## Reserved model names: Directory → DirectoryObject1 + +- **Class:** kiota-symbol-prediction +- **Status:** handled (observed rule encoded, pinned test) +- **Evidence:** kiota renames model classes on its C# reserved list (BCL conflicts) by + appending `Object`, then dedupes numerically: `microsoft.graph.directory` generates as + `DirectoryObject1` because `directoryObject` already exists (Identity.DirectoryManagement). + A bare `Directory` reference had first resolved to `System.IO.Directory` under implicit + usings. +- **Decision:** encode the observed rule for reserved names present in Graph docs + (Directory/File/Task/Type/Environment), computed against the document's schema set. This + mirrors observed kiota 1.32.2 behavior — a wrong prediction fails the module compile, it + cannot fail silently. +- **Migration impact:** none — internal type references only. +- **References:** kiota's CSharpReservedClassNamesProvider; + PowerShellWrapperGenerationService.KiotaReservedModelNames. + +## A model that shares its name with a kiota sub-namespace moves inside it + +- **Class:** kiota-symbol-prediction +- **Status:** handled (all model references fully qualified; pinned tests) +- **Evidence:** `microsoft.graph.security` generates as `Models.Security.Security` because + the `microsoft.graph.security.*` family creates a `Models.Security` namespace; bare + `Security` resolved to the namespace and did not compile (Security module; same for + `partners`/`Models.Partners.Partners` in Reports). +- **Decision:** fully qualify every model type reference and mirror the move-inside rule + when a single-segment name matches a sub-namespace derived from the document. +- **Migration impact:** none. + +## kiota strips underscores from member names + +- **Class:** kiota-symbol-prediction +- **Status:** handled (pinned test) +- **Evidence:** signIn's `riskEventTypes_v2` property generates as `RiskEventTypesV2`; the + wrapper's naive Pascal-casing produced `RiskEventTypes_v2` and the body assignment did + not compile (Reports). +- **Decision:** mirror the cleanup (drop `_`, upper-case the following character) when + naming the model member a body parameter assigns to. +- **Migration impact:** none. + +## Query options exist only where the doc declares them + +- **Class:** kiota-builder-shape +- **Status:** handled (pinned by the option-table mechanism) +- **Evidence:** kiota omits query-parameter properties the operation doesn't declare: + content/stream endpoints get a bare `DefaultQueryParameters` (Files, Notes, Users, +4 on + the styled docs), and `subscribedSkus/{id}` declares `$select` but not `$expand` + (Identity.DirectoryManagement, KiotaCompat). Unconditional `-Property`/`-ExpandProperty` + bindings did not compile. +- **Decision:** item GETs and dispatchers now emit `$select`/`$expand` parameters only when + the operation declares them, per parameter set — the same declared-options mechanism list + GETs already used. +- **Migration impact:** cmdlets for endpoints without `$select`/`$expand` no longer expose + dead `-Property`/`-ExpandProperty` parameters (they never worked server-side). + +## Media/content endpoints return Stream regardless of declared JSON schema + +- **Class:** kiota-builder-shape +- **Status:** handled (skipped with a logged reason; pinned by regression test) +- **Evidence:** the styled docs attach an entity JSON schema to media endpoints + (`.../filesFolder/content`), but kiota generates `GetAsync` returning `System.IO.Stream` + for them — assigning that to an entity type does not compile (Teams, built from the + styled doc because kiota hangs on its KiotaCompat variant). The KiotaCompat docs declare + these endpoints without a JSON schema, so they were already skipped there. +- **Decision:** a GET whose success response also declares non-JSON content is a media + download and is skipped until stream support exists (same treatment as `$value`). +- **Migration impact:** content-download cmdlets (`Get-...Content`) are not generated yet; + tracked with the operation-shapes work. + +## PATCH-only resources have no GetAsync to re-fetch + +- **Class:** kiota-builder-shape +- **Status:** handled (pinned test) +- **Evidence:** Update cmdlets re-fetch after a bodiless 204; `/places/{place-id}` has no + GET, so the builder has no `GetAsync` and `Update-MgPlace` did not compile (Calendar). +- **Decision:** emit the re-fetch only when the path declares a GET; otherwise a bodiless + 204 returns nothing — matching the published SDK's Update behavior. +- **Migration impact:** none vs the published SDK. diff --git a/tools/WrapperGenerator/edge-cases/naming-edge-cases.md b/tools/WrapperGenerator/edge-cases/naming-edge-cases.md index 0c70d0a06e6..e5d541e5621 100644 --- a/tools/WrapperGenerator/edge-cases/naming-edge-cases.md +++ b/tools/WrapperGenerator/edge-cases/naming-edge-cases.md @@ -46,6 +46,7 @@ Entry template (keep the field names exact so the file converts cleanly): | Case | Class | Status | |---|---|---| | `HostWhoi` → `HostWhois` | inflection-defect | corrected | +| `PlaceCheck` → `PlaceCheckIn` | operationid-truncation | corrected | | operationId preposition truncation | operationid-truncation | structurally-avoided | | `SkypeForBusiness` subject truncation | operationid-truncation | not-yet-reachable | | `Cookies`/`Skus`/`Dns`/`Ios`/`Statistics` quirks | inflection-defect | reproduced-for-parity | @@ -69,6 +70,22 @@ Entry template (keep the field names exact so the file converts cleanly): - **References:** pinned in `AppliesDeliberateNameCorrections` (NamingTests.cs); gate rows in `$deliberateCorrections` (Compare-WrapperCmdletNames.ps1). +## CheckIns truncated to Check on the places API + +- **Class:** operationid-truncation +- **Status:** corrected +- **Evidence:** `/places/{place-id}/checkIns` shipped as `{Get,New,Update,Remove}-Mg(Beta)PlaceCheck` + (8 commands) — AutoRest truncated "CheckIns" at the preposition "In", the #912 defect + class. The SDK is inconsistent with itself: `Get-MgPlaceCheckInCount` (the `$count` path) + keeps "In" intact. Found by the parity gate during the full-inventory module fan-out. +- **Decision:** emit `...PlaceCheckIn` for all four verbs, v1.0 and beta; no alias for the + old names. Pinned in `AppliesDeliberateNameCorrections`; gate rows in + `$deliberateCorrections`. +- **Migration impact:** scripts using `*-MgPlaceCheck` must switch to `*-MgPlaceCheckIn`. + Belongs in the migration guide when the Calendar module ships for real. +- **References:** issue [#912](https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/912) + (the AutoRest defect class). + ## operationId preposition/linking-verb truncation - **Class:** operationid-truncation @@ -130,9 +147,8 @@ Entry template (keep the field names exact so the file converts cleanly): Cases spotted but deliberately not acted on yet, so they aren't lost: -- **`usageRights` vs `rights` (beta-only):** the shipped SDK keeps `usageRights` plural - (`Get-MgBetaDeviceUsageRights` for `/devices/{id}/usageRights`) but singularizes bare - `rights` (`Get-MgBetaGroupSiteInformationProtectionSensitivityLabelRight` for - `.../sensitivityLabels/{id}/rights`). Our rules match the bare-`rights` case and would - diverge on `usageRights`. All affected paths are beta; resolve when the beta parity audit - runs. +- **bare `rights` (beta-only):** "Rights" is now an invariant — v1.0 evidence arrived via + `subjectRightsRequests` (42 cmdlets ship keeping "Rights"; `usageRights` in beta agrees). + The one holdout is beta's bare `.../sensitivityLabels/{id}/rights`, which ships + singularized (`...SensitivityLabelRight`) and now diverges from our invariant. Beta-only, + 4 cmdlets; resolve at the beta parity audit (likely a correction or a path override). From 668a28d03358c6fade2c2017570ea1eaa6272877 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Tue, 11 Aug 2026 10:25:40 -0700 Subject: [PATCH 07/13] fix(wrapper-generator): fail loudly on cmdlet file collisions A second operation resolving to an already-written cmdlet file now fails generation with the full collision list instead of silently overwriting it, which is the silent-drop failure mode AutoRest had. OData cast list/item pairs (owners/graph.user) now merge like plain pairs, and the sweep's collisions land as cited NamingOverrides entries: termStore and agreement-file stitches, default-singleton renames (SubSite, DefaultDrive, DefaultCalendarEvent), and nested navs the SDK never shipped. Remaining families are tracked on #3704. --- tools/Build-WrapperModule.ps1 | 14 +- .../GenerationServiceRegressionTests.cs | 77 ++++++++ tools/WrapperGenerator.Tests/NamingTests.cs | 66 ++++++- tools/WrapperGenerator/CmdletNaming.cs | 52 ++++- tools/WrapperGenerator/NamingOverrides.cs | 182 ++++++++++++++++-- .../PowerShellWrapperGenerationService.cs | 33 +++- tools/WrapperGenerator/README.md | 6 +- .../edge-cases/naming-edge-cases.md | 43 ++++- 8 files changed, 440 insertions(+), 33 deletions(-) diff --git a/tools/Build-WrapperModule.ps1 b/tools/Build-WrapperModule.ps1 index dc67dc464c8..3a5a392c1dd 100644 --- a/tools/Build-WrapperModule.ps1 +++ b/tools/Build-WrapperModule.ps1 @@ -143,7 +143,19 @@ function Build-OneModule { } $wrapperOut = & dotnet run --project $generatorProject -- -d $spec -o $cmdletsDir -n $clientNs 2>&1 - if ($LASTEXITCODE -ne 0) { $result.FailedAt = 'wrapper-generator'; $result.Error = ($wrapperOut | Select-Object -Last 3) -join ' | '; return $result } + if ($LASTEXITCODE -ne 0) { + # Skip warnings precede the failure; the exception message is what identifies it. + $result.FailedAt = 'wrapper-generator' + $lines = @($wrapperOut | ForEach-Object { "$_" }) + $exception = $lines | Where-Object { $_ -match 'Unhandled exception|Exception:' } | Select-Object -First 1 + $exceptionIndex = if ($exception) { $lines.IndexOf($exception) } else { -1 } + $result.Error = if ($exceptionIndex -ge 0) { + ($lines[$exceptionIndex..([Math]::Min($exceptionIndex + 5, $lines.Count - 1))] | Where-Object { $_ -notmatch '^\s+at ' }) -join ' | ' + } else { + ($lines | Where-Object { $_ -notmatch '^\s+at ' } | Select-Object -First 6) -join ' | ' + } + return $result + } # Generated artifact, machine-local by design (absolute reference into this clone). $csprojPath = Join-Path $srcDir "$moduleName.csproj" diff --git a/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs b/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs index 7275265a81c..b63d68d2443 100644 --- a/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs +++ b/tools/WrapperGenerator.Tests/GenerationServiceRegressionTests.cs @@ -181,6 +181,83 @@ public async Task GenerateAsync_SkipsUnsupportedODataPathSegments_DoesNotEmitMal Assert.DoesNotContain(files, f => f != "Shared.g.cs"); } + // Two operations resolving to the same cmdlet file must fail generation loudly, + // identifying both operations — never silently overwrite. The real shipped collision + // (/sites/{id}/sites) is renamed via NamingOverrides, so a synthetic self-referential + // path keeps the guard itself exercised. + [Fact] + public async Task GenerateAsync_FailsLoudlyWhenTwoCmdletsResolveToTheSameFile() + { + var document = new OpenApiDocument + { + Paths = new OpenApiPaths(), + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["microsoft.graph.widget"] = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["displayName"] = new OpenApiSchema { Type = JsonSchemaType.String }, + }, + }, + }, + }, + }; + + static OpenApiOperation ItemGet(OpenApiDocument doc) => new() + { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("microsoft.graph.widget", doc), + }, + }, + }, + }, + }; + + // /widgets/{id} and /widgets/{id}/widgets/{id2} both singularize to the noun Widget; + // with two same-noun item GETs nothing merges, and both emit GetMgWidget.g.cs. + document.Paths["/widgets/{widget-id}"] = new OpenApiPathItem + { + Operations = new Dictionary { [HttpMethod.Get] = ItemGet(document) }, + }; + document.Paths["/widgets/{widget-id}/widgets/{widget-id1}"] = new OpenApiPathItem + { + Operations = new Dictionary { [HttpMethod.Get] = ItemGet(document) }, + }; + + var outputDir = Path.Combine(Path.GetTempPath(), "wrapper-generator-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(outputDir); + try + { + var config = new GeneratorConfig("Microsoft.Graph.PowerShell.Test.Client", outputDir); + var service = new PowerShellWrapperGenerationService(document, config, NullLogger.Instance); + + var ex = await Assert.ThrowsAsync(() => service.GenerateAsync(CancellationToken.None)); + + Assert.Contains("GetMgWidget.g.cs", ex.Message); + Assert.Contains("collision", ex.Message); + // Both colliding cmdlets are named Get-MgWidget, so only their builder expressions + // prove the message identifies both operations. + Assert.Contains("[Widgets[WidgetId]]", ex.Message); + Assert.Contains("Widgets[WidgetId].Widgets[WidgetId1]", ex.Message); + } + finally + { + if (Directory.Exists(outputDir)) + Directory.Delete(outputDir, recursive: true); + } + } + private static OpenApiDocument BuildDocument(HttpMethod method, string path, OpenApiOperation operation) { return new OpenApiDocument diff --git a/tools/WrapperGenerator.Tests/NamingTests.cs b/tools/WrapperGenerator.Tests/NamingTests.cs index 3f101ae7522..3e8c8ebca65 100644 --- a/tools/WrapperGenerator.Tests/NamingTests.cs +++ b/tools/WrapperGenerator.Tests/NamingTests.cs @@ -110,6 +110,19 @@ private static CmdletNaming Resolve(string method, string path) => [InlineData("GET", "/solutions/bookingBusinesses/{bookingBusiness-id}", "Get", "MgBookingBusiness")] [InlineData("PATCH", "/solutions/bookingBusinesses/{bookingBusiness-id}", "Update", "MgBookingBusiness")] [InlineData("GET", "/users/{user-id}/calendar", "Get", "MgUserDefaultCalendar")] + // self-referential sites rename to SubSite instead of colliding with the parent noun + [InlineData("GET", "/sites/{site-id}/sites", "Get", "MgSubSite")] + [InlineData("GET", "/sites/{site-id}/sites/{site-id1}", "Get", "MgSubSite")] + [InlineData("GET", "/groups/{group-id}/sites/{site-id}/sites", "Get", "MgGroupSubSite")] + [InlineData("GET", "/groups/{group-id}/sites/{site-id}/sites/{site-id1}", "Get", "MgGroupSubSite")] + // default-singleton renames (issue #3704 oracle sweep) + [InlineData("GET", "/users/{user-id}/drive", "Get", "MgUserDefaultDrive")] + [InlineData("GET", "/groups/{group-id}/drive", "Get", "MgGroupDefaultDrive")] + [InlineData("GET", "/sites/{site-id}/drive", "Get", "MgSiteDefaultDrive")] + [InlineData("GET", "/groups/{group-id}/sites/{site-id}/drive", "Get", "MgGroupSiteDefaultDrive")] + [InlineData("GET", "/users/{user-id}/calendar/events", "Get", "MgUserDefaultCalendarEvent")] + // nested-collection GET renamed by the Groups.md directive (subject $1ByGroup) + [InlineData("GET", "/groups/{group-id}/groupLifecyclePolicies", "Get", "MgGroupLifecyclePolicyByGroup")] // boundary word-overlap collapse (Get-MgDomainNameReference) [InlineData("GET", "/domains/{domain-id}/domainNameReferences", "Get", "MgDomainNameReference")] // adjacent-duplicate collapse (Get-MgUserOnenoteSectionGroup... family) @@ -177,6 +190,34 @@ public void SuppressesOperationsThePublishedSdkOmits() Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/solutions")); Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Patch, "/solutions")); Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/solutions/bookingBusinesses/{bookingBusiness-id}")); + + // The /photos collection ships no distinct cmdlet; only the /photo singleton does. + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/users/{user-id}/photos")); + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/users/{user-id}/photos/{userProfilePhoto-id}")); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/users/{user-id}/photo")); + + // Suffix-matched suppressions apply under any root; siblings stay generated + // (issue #3704: Info-wrapper navs ship nothing, their siblings ship). + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/chats/{chat-id}/pinnedMessages/{pinnedChatMessageInfo-id}/message")); + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/teams/{team-id}/channels/{channel-id}/sharedWithTeams/{id}/team")); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/teams/{team-id}/channels/{channel-id}/sharedWithTeams/{id}/allowedMembers")); + + // Exact-matched suppressions cover only the named node; descendants with no entry of + // their own stay generated (Security nested navs, issue #3704). + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/security/threatIntelligence/hosts/{host-id}/components")); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/security/threatIntelligence/hosts/{host-id}/components/$count")); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/security/threatIntelligence/hosts/{host-id}/passiveDns")); + + // termStore trees are stitched: /termStores/{id} descendants ship nothing (the 402 + // descendant command rows come from the /termStore singleton trees), and the singleton + // root GET ships no distinct cmdlet (Get-MgSiteTermStore serves both /termStore and + // /termStores; GET generates from the collection side only). + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/sites/{site-id}/termStores/{store-id}")); + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/sites/{site-id}/termStores/{store-id}/sets/{set-id}")); + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/sites/{site-id}/termStore")); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Patch, "/sites/{site-id}/termStore")); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/sites/{site-id}/termStores")); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/sites/{site-id}/termStore/sets/{set-id}")); } [Theory] @@ -228,14 +269,29 @@ public void GetWithNoStructuralPartnerStaysStandalone() } [Fact] - public void AmbiguousSameNounDoesNotMerge() + public void CastListItemPairMergesLikeAPlainPair() + { + // The published SDK ships one Get-MgGroupOwnerAsUser covering both the cast on the + // collection and the cast on the item; without pairing, both emit the same file. + var list = Resolve("GET", "/groups/{group-id}/owners/graph.user"); + var item = Resolve("GET", "/groups/{group-id}/owners/{directoryObject-id}/graph.user"); + Assert.Equal(list.Noun, item.Noun); + Assert.True(Naming.IsListItemPair(list, item)); + + // Different cast types never pair. + var otherCast = Resolve("GET", "/groups/{group-id}/owners/{directoryObject-id}/graph.servicePrincipal"); + Assert.False(Naming.IsListItemPair(list, otherCast)); + } + + [Fact] + public void SelfReferentialSitesRenameInsteadOfCollidingWithParent() { - // Self-referential /sites: the collection /sites/{id}/sites and the single /sites/{id} - // both resolve to MgSite, but the "item" is the parent, not a child one id deeper, so - // the structural check rejects the merge and both stay standalone. + // Without the SubSite rename, /sites/{id}/sites singularizes to the parent's own noun + // and its cmdlet file would collide with Get-MgSite's. The renamed nouns are pinned in + // ResolvesPublishedSdkNames; this pins that the pair no longer merges or collides. var list = Resolve("GET", "/sites/{site-id}/sites"); var item = Resolve("GET", "/sites/{site-id}"); - Assert.Equal(list.Noun, item.Noun); + Assert.NotEqual(list.Noun, item.Noun); Assert.False(Naming.IsListItemPair(list, item)); } } diff --git a/tools/WrapperGenerator/CmdletNaming.cs b/tools/WrapperGenerator/CmdletNaming.cs index 6cf4339687c..a9836723e2e 100644 --- a/tools/WrapperGenerator/CmdletNaming.cs +++ b/tools/WrapperGenerator/CmdletNaming.cs @@ -197,16 +197,54 @@ private static string ToCastAwareBuilderMemberName(string segment) => : segment.ToFirstCharacterUpperCase(); // Whether a list GET and an item GET form a mergeable pair for the public Get-MgX - // dispatcher: the item's path must be the list's path plus exactly one trailing id - // (Users[UserId].Messages -> Users[UserId].Messages[MessageId]). Callers group by noun - // first, so this only decides the structural fit; a same-noun item that does not extend the - // list (for example the self-referential /sites/{id} vs /sites/{id}/sites) is rejected. + // dispatcher: the item's path must extend the list's path by exactly one id, either + // trailing (Users[UserId].Messages -> Users[UserId].Messages[MessageId]) or inserted + // before a shared trailing OData cast (Owners.GraphUser -> Owners[Id].GraphUser). Callers + // group by noun first, so this only decides the structural fit; a same-noun item that does + // not extend the list is rejected. public static bool IsListItemPair(CmdletNaming list, CmdletNaming item) { ArgumentNullException.ThrowIfNull(list); ArgumentNullException.ThrowIfNull(item); - return item.PathParamNames.Count == list.PathParamNames.Count + 1 - && item.PathParamNames.Take(list.PathParamNames.Count).SequenceEqual(list.PathParamNames) - && item.BuilderExpression.StartsWith(list.BuilderExpression + "[", StringComparison.Ordinal); + if (item.PathParamNames.Count != list.PathParamNames.Count + 1 + || !item.PathParamNames.Take(list.PathParamNames.Count).SequenceEqual(list.PathParamNames)) + return false; + + if (item.BuilderExpression.StartsWith(list.BuilderExpression + "[", StringComparison.Ordinal)) + return true; + + // OData cast pair: the id inserts BEFORE the trailing cast member, not at the end + // (owners/graph.user vs owners/{id}/graph.user builds Owners.GraphUser vs + // Owners[Id].GraphUser). The published SDK ships these as one cmdlet, same as a + // plain list/item pair; without this the two emit identical file names and collide. + var listCast = TrailingCastMember(list.BuilderExpression); + var itemCast = TrailingCastMember(item.BuilderExpression); + if (listCast is null || !string.Equals(listCast, itemCast, StringComparison.Ordinal)) + return false; + + var listStem = list.BuilderExpression[..^(listCast.Length + 1)]; + var itemStem = item.BuilderExpression[..^(itemCast.Length + 1)]; + if (!itemStem.StartsWith(listStem + "[", StringComparison.Ordinal) || !itemStem.EndsWith("]", StringComparison.Ordinal)) + return false; + var indexer = itemStem[(listStem.Length + 1)..^1]; + return indexer.Length > 0 && !indexer.Contains('[') && !indexer.Contains('.'); + } + + // The kiota builder member for a trailing OData cast segment (GraphUser from + // "graph.user", MicrosoftGraphUser from "microsoft.graph.user"); null when the + // expression does not end in a cast. + private static string? TrailingCastMember(string builderExpression) + { + var lastDot = builderExpression.LastIndexOf('.'); + if (lastDot < 0) + return null; + var member = builderExpression[(lastDot + 1)..]; + if (member.Contains('[')) + return null; + if (member.StartsWith("MicrosoftGraph", StringComparison.Ordinal) && member.Length > 14 && char.IsUpper(member[14])) + return member; + if (member.StartsWith("Graph", StringComparison.Ordinal) && member.Length > 5 && char.IsUpper(member[5])) + return member; + return null; } } diff --git a/tools/WrapperGenerator/NamingOverrides.cs b/tools/WrapperGenerator/NamingOverrides.cs index db36576a211..3aae42bfc8d 100644 --- a/tools/WrapperGenerator/NamingOverrides.cs +++ b/tools/WrapperGenerator/NamingOverrides.cs @@ -7,12 +7,13 @@ namespace WrapperGenerator; // Hand-tuned naming exceptions, kept as data with a cited source on every entry. // -// The published Microsoft.Graph names are mostly algorithmic, but a few come from -// hand-written AutoRest directives in the msgraph-sdk-powershell module configs. Matching -// the published names 100% means mirroring those directives here. +// The published Microsoft.Graph names are mostly algorithmic. Entries here cover the rest: +// renames from hand-written AutoRest directives in the msgraph-sdk-powershell module +// configs, and suppressions for spec routes the published SDK ships nothing for. // -// Keep this list short. Add an entry only when the published name cannot come out of the -// naming rules, and cite the directive that created it. +// Add an entry only when the published surface cannot come out of the naming rules, and +// cite the evidence: the directive when one exists, otherwise the shipped-command +// inventory (the oracle, MgCommandMetadata.json). public static partial class NamingOverrides { private enum OverrideKind @@ -22,33 +23,185 @@ private enum OverrideKind StripNounPrefix, } - private sealed record Entry(OverrideKind Kind, HttpMethod? Method, string PathPrefix, bool ExactPath, string? Value, string Reason); + // How Pattern is matched against the normalized path: the full path (Exact), its start + // (Prefix), or its end (Suffix — for navs that recur under many roots, like + // .../resourceRoleScopes/{}/scope appearing under several parents). + private enum PathMatch + { + Exact, + Prefix, + Suffix, + } + + private sealed record Entry(OverrideKind Kind, HttpMethod? Method, string Pattern, PathMatch Match, string? Value, string Reason); private static readonly List Entries = [ // The SDK ships no Update cmdlet for /users/{id}/calendar. Its pipeline removes the // operation outright, in src/Calendar/Calendar.md: remove-path-by-operation // user_UpdateCalendar. The wrapper must not invent a cmdlet the SDK chose to drop. - new(OverrideKind.SuppressOperation, HttpMethod.Patch, "/users/{}/calendar", ExactPath: true, Value: null, + new(OverrideKind.SuppressOperation, HttpMethod.Patch, "/users/{}/calendar", Match: PathMatch.Exact, Value: null, Reason: "Calendar.md remove-path-by-operation user_UpdateCalendar"), // GET /users/{id}/calendar ships as Get-MgUserDefaultCalendar, renamed in // src/Calendar/Calendar.md: "^(User)(Calendar)$" -> "$1Default$2". - new(OverrideKind.ReplaceNoun, HttpMethod.Get, "/users/{}/calendar", ExactPath: true, Value: "UserDefaultCalendar", + new(OverrideKind.ReplaceNoun, HttpMethod.Get, "/users/{}/calendar", Match: PathMatch.Exact, Value: "UserDefaultCalendar", Reason: "Calendar.md directive renames UserCalendar to UserDefaultCalendar"), // The SDK ships no cmdlets for the /solutions root singleton itself (Get-MgSolution / // Update-MgSolution do not exist): src/Bookings/Bookings.md removes every solutionsRoot // operation with remove-path-by-operation ^solution\.solutionsRoot.*$. Exact-path, all // methods, so operations on children like /solutions/bookingBusinesses are unaffected. - new(OverrideKind.SuppressOperation, Method: null, "/solutions", ExactPath: true, Value: null, + new(OverrideKind.SuppressOperation, Method: null, "/solutions", Match: PathMatch.Exact, Value: null, Reason: "Bookings.md remove-path-by-operation ^solution\\.solutionsRoot.*$"), // Most nouns under /solutions/ drop the "Solution" prefix (for example // Get-MgBookingBusiness, Get-MgVirtualEventWebinar). BackupRestore is a known // exception where published cmdlets keep the Solution prefix. - new(OverrideKind.StripNounPrefix, Method: null, "/solutions/", ExactPath: false, Value: "Solution", + new(OverrideKind.StripNounPrefix, Method: null, "/solutions/", Match: PathMatch.Prefix, Value: "Solution", Reason: "Bookings/VirtualEvents naming pattern under /solutions/*; BackupRestore is explicitly excluded in ApplyNounOverrides"), + + // The spec carries two parallel termStore trees; the shipped surface stitches them: + // GET/POST come from the /termStores collection (Get-MgSiteTermStore, New-...), while + // PATCH/DELETE and all 402 descendant command rows come from the /termStore singleton. + // Nothing ships under /termStores/{id}, and the singleton root GET has no distinct + // cmdlet — generating either would collide with its shipped twin. + new(OverrideKind.SuppressOperation, Method: null, "/sites/{}/termstores/{}", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: zero commands under /termStores/{id}; descendants ship from the /termStore singleton tree"), + new(OverrideKind.SuppressOperation, HttpMethod.Get, "/sites/{}/termstore", Match: PathMatch.Exact, Value: null, + Reason: "oracle: Get-MgSiteTermStore serves GET /termStore and /termStores; GET generates from the collection side only"), + new(OverrideKind.SuppressOperation, Method: null, "/groups/{}/sites/{}/termstores/{}", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: zero commands under /termStores/{id}; descendants ship from the /termStore singleton tree"), + new(OverrideKind.SuppressOperation, HttpMethod.Get, "/groups/{}/sites/{}/termstore", Match: PathMatch.Exact, Value: null, + Reason: "oracle: Get-MgGroupSiteTermStore serves GET /termStore and /termStores; GET generates from the collection side only"), + + // Get-MgUserPhoto serves both /photo and /photos; the /photos routes ship no distinct + // cmdlet, and generating them would collide with the singleton's noun. + new(OverrideKind.SuppressOperation, Method: null, "/users/{}/photos", Match: PathMatch.Exact, Value: null, + Reason: "oracle: Get-MgUserPhoto serves /photo and /photos; the collection ships no distinct cmdlet"), + new(OverrideKind.SuppressOperation, Method: null, "/users/{}/photos/{}", Match: PathMatch.Exact, Value: null, + Reason: "oracle: /photos/{} ships nothing; the photo surface is the /photo singleton"), + + // Self-referential /sites: singularizing sites/{id}/sites collapses to the parent's + // noun, so the sub-sites cmdlets would overwrite Get-MgSite. The SDK ships them + // renamed: Get-MgSubSite and Get-MgGroupSubSite (v1.0 and beta, incl. $count). + new(OverrideKind.ReplaceNoun, Method: null, "/sites/{}/sites", Match: PathMatch.Exact, Value: "SubSite", + Reason: "Sites.md directive; oracle ships Get-MgSubSite for /sites/{site-id}/sites"), + new(OverrideKind.ReplaceNoun, Method: null, "/sites/{}/sites/{}", Match: PathMatch.Exact, Value: "SubSite", + Reason: "Sites.md directive; oracle ships Get-MgSubSite for /sites/{site-id}/sites/{site-id1}"), + new(OverrideKind.ReplaceNoun, Method: null, "/groups/{}/sites/{}/sites", Match: PathMatch.Exact, Value: "GroupSubSite", + Reason: "Sites.md directive; oracle ships Get-MgGroupSubSite"), + new(OverrideKind.ReplaceNoun, Method: null, "/groups/{}/sites/{}/sites/{}", Match: PathMatch.Exact, Value: "GroupSubSite", + Reason: "Sites.md directive; oracle ships Get-MgGroupSubSite"), + + // ---- Collision resolutions from the full-inventory oracle sweep (issue #3704). ---- + + // Identity.Governance: agreement file item ops ship only from the /file singleton + // (Update/Remove-MgAgreementFile); /files/{} items ship nothing, /files/{}/versions does. + new(OverrideKind.SuppressOperation, Method: null, "/agreements/{}/files/{}", Match: PathMatch.Exact, Value: null, + Reason: "oracle: /files/{} item ops ship nothing; file surface is the /file singleton (Update/Remove-MgAgreementFile)"), + new(OverrideKind.SuppressOperation, Method: null, "/identitygovernance/termsofuse/agreements/{}/files/{}", Match: PathMatch.Exact, Value: null, + Reason: "oracle: ships nothing; mirrors /agreements/{}/files/{} suppression"), + // GET of the file/files pair ships from the collection (same command on both URIs), + // like the termStore root stitch; Update/Remove stay on the singleton. + new(OverrideKind.SuppressOperation, HttpMethod.Get, "/agreements/{}/file", Match: PathMatch.Exact, Value: null, + Reason: "oracle: Get-MgAgreementFile serves /file and /files; GET generated from the collection side only"), + new(OverrideKind.SuppressOperation, HttpMethod.Get, "/identitygovernance/termsofuse/agreements/{}/file", Match: PathMatch.Exact, Value: null, + Reason: "oracle: Get-MgIdentityGovernanceTermsOfUseAgreementFile serves /file and /files; GET from the collection side only"), + // The /scope node duplicates its parent's noun and ships nothing anywhere; its + // children ship with the Scope segment elided (…ResourceRoleScopeResource). + new(OverrideKind.SuppressOperation, Method: null, "/resourcerolescopes/{}/scope", Match: PathMatch.Suffix, Value: null, + Reason: "oracle: the /scope node ships nothing under any parent; children ship with Scope elided"), + new(OverrideKind.SuppressOperation, Method: null, "/identitygovernance/entitlementmanagement/assignments/{}/assignmentpolicy", Match: PathMatch.Exact, Value: null, + Reason: "nav duplicate of /assignmentPolicies (Get-MgEntitlementManagementAssignmentPolicy); ships nothing"), + new(OverrideKind.SuppressOperation, Method: null, "/identitygovernance/entitlementmanagement/resources/{}/environment", Match: PathMatch.Exact, Value: null, + Reason: "nav duplicate of /resourceEnvironments (Get-MgEntitlementManagementResourceEnvironment); ships nothing"), + new(OverrideKind.SuppressOperation, Method: null, "/identitygovernance/lifecycleworkflows", Match: PathMatch.Exact, Value: null, + Reason: "the container node's own operations ship nothing; its children ship"), + new(OverrideKind.SuppressOperation, Method: null, "/identitygovernance/termsofuse/agreements/{}/acceptances", Match: PathMatch.Prefix, Value: null, + Reason: "ships nothing; acceptances ship from /termsOfUse/agreementAcceptances (Get-MgIdentityGovernanceTermsOfUseAgreementAcceptance)"), + + // Security threatIntelligence: nested navs under articles/{} and hosts/{} duplicate + // the shipped top-level sets (articleIndicators, hostComponents, hostCookies, + // hostPairs, hostPorts, hostSslCertificates, hostTrackers) and ship nothing + // themselves. Exact-only: two of these navs have shipped $count children + // (Get-MgSecurityThreatIntelligenceHost{SslCertificate,Tracker}Count); the other five + // ship no children at all. + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/articles/{}/indicators", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level articleIndicators ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/articles/{}/indicators/{}", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level articleIndicators ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/components", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostComponents ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/components/{}", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostComponents ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/cookies", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostCookies ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/cookies/{}", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostCookies ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/hostpairs", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostPairs ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/hostpairs/{}", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostPairs ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/ports", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostPorts ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/ports/{}", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostPorts ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/sslcertificates", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostSslCertificates ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/sslcertificates/{}", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostSslCertificates ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/trackers", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostTrackers ships"), + new(OverrideKind.SuppressOperation, Method: null, "/security/threatintelligence/hosts/{}/trackers/{}", Match: PathMatch.Exact, Value: null, Reason: "oracle: ships nothing; top-level hostTrackers ships"), + // The attackSimulation container node itself ships nothing; children under a + // simulation item ship nothing either (the list/item pair then merges normally). + new(OverrideKind.SuppressOperation, Method: null, "/security/attacksimulation", Match: PathMatch.Exact, Value: null, + Reason: "oracle: the container node ships nothing; its child collections ship"), + new(OverrideKind.SuppressOperation, Method: null, "/security/attacksimulation/simulations/{}/", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: nothing under a simulation item ships in v1.0"), + + // Calendar: the shipped default-calendar surface. Events under a NAMED calendar and + // the default-calendar event item tree ship nothing; event items ship from + // /users/{}/events (Get-MgUserEvent family). + new(OverrideKind.ReplaceNoun, Method: null, "/users/{}/calendar/events", Match: PathMatch.Exact, Value: "UserDefaultCalendarEvent", + Reason: "oracle: list/create ship as Get/New-MgUserDefaultCalendarEvent"), + new(OverrideKind.SuppressOperation, Method: null, "/users/{}/calendar/events/{}", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: default-calendar event items ship nothing; items ship from /users/{}/events/{}"), + new(OverrideKind.SuppressOperation, Method: null, "/users/{}/calendars/{}/events/{}", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: named-calendar event items ship nothing; items ship from /users/{}/events/{}"), + new(OverrideKind.SuppressOperation, Method: null, "/users/{}/calendars/{}/calendarpermissions", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: permissions ship only from the default calendar (Get-MgUserCalendarPermission on /users/{}/calendar/calendarPermissions)"), + + // Teams Info-wrapper navs: the wrapped single-entity navigation ships nothing under + // any root. The suffix matches just the nav node, so shipped siblings + // (…SharedWithTeamAllowedMember) are unaffected. + new(OverrideKind.SuppressOperation, Method: null, "/pinnedmessages/{}/message", Match: PathMatch.Suffix, Value: null, + Reason: "oracle: ships nothing under any root; list side ships (Get-MgChatPinnedMessage)"), + new(OverrideKind.SuppressOperation, Method: null, "/sharedwithteams/{}/team", Match: PathMatch.Suffix, Value: null, + Reason: "oracle: ships nothing; sibling /allowedMembers ships, so node-only"), + new(OverrideKind.SuppressOperation, Method: null, "/associatedteams/{}/team", Match: PathMatch.Suffix, Value: null, + Reason: "oracle: ships nothing; list side ships (Get-MgUserTeamworkAssociatedTeam)"), + + // Groups: the nested lifecycle-policies GET ships renamed; everything else on that + // route ships from the top-level set. Photos items ship nothing (singleton /photo). + new(OverrideKind.ReplaceNoun, HttpMethod.Get, "/groups/{}/grouplifecyclepolicies", Match: PathMatch.Exact, Value: "GroupLifecyclePolicyByGroup", + Reason: "Groups.md directive (subject $1ByGroup); oracle ships Get-MgGroupLifecyclePolicyByGroup"), + new(OverrideKind.SuppressOperation, Method: null, "/groups/{}/grouplifecyclepolicies/", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: item/children under the nested route ship nothing; the set ships top-level"), + new(OverrideKind.SuppressOperation, Method: null, "/groups/{}/photos/{}", Match: PathMatch.Exact, Value: null, + Reason: "oracle: photos items ship nothing; shipped surface is the /photo singleton (Get-MgGroupPhoto)"), + + // Small-module resolutions. + new(OverrideKind.SuppressOperation, Method: null, "/solutions/virtualevents", Match: PathMatch.Exact, Value: null, + Reason: "oracle: the virtualEvents root node ships nothing; children ship (Get-MgVirtualEventWebinar)"), + new(OverrideKind.SuppressOperation, Method: null, "/replies/{}/replyto", Match: PathMatch.Suffix, Value: null, + Reason: "oracle: the replyTo nav ships nothing under any root"), + new(OverrideKind.SuppressOperation, Method: null, "/deviceappmanagement/mobileapps/{}/categories", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: nested app categories ship nothing; the set ships top-level (mobileAppCategories)"), + new(OverrideKind.SuppressOperation, Method: null, "/education/classes/{}/assignments/{}/categories", Match: PathMatch.Exact, Value: null, + Reason: "oracle: the plain path ships nothing; the shipped surface is the $ref route"), + new(OverrideKind.SuppressOperation, Method: null, "/education/users/{}/user", Match: PathMatch.Exact, Value: null, + Reason: "oracle: the user self-nav node ships nothing; its children (mailboxSettings) ship"), + new(OverrideKind.ReplaceNoun, HttpMethod.Get, "/groups/{}/drive", Match: PathMatch.Exact, Value: "GroupDefaultDrive", + Reason: "Files.md directive (subject $1Default$2); oracle ships Get-MgGroupDefaultDrive"), + new(OverrideKind.ReplaceNoun, HttpMethod.Get, "/users/{}/drive", Match: PathMatch.Exact, Value: "UserDefaultDrive", + Reason: "Files.md directive (subject $1Default$2); oracle ships Get-MgUserDefaultDrive"), + new(OverrideKind.ReplaceNoun, HttpMethod.Get, "/sites/{}/drive", Match: PathMatch.Exact, Value: "SiteDefaultDrive", + Reason: "oracle ships Get-MgSiteDefaultDrive for the site default-drive singleton"), + new(OverrideKind.ReplaceNoun, HttpMethod.Get, "/groups/{}/sites/{}/drive", Match: PathMatch.Exact, Value: "GroupSiteDefaultDrive", + Reason: "oracle ships Get-MgGroupSiteDefaultDrive"), + new(OverrideKind.SuppressOperation, HttpMethod.Get, "/shares/{}/list/items/{}", Match: PathMatch.Exact, Value: null, + Reason: "oracle: the bare shared-list item GET ships nothing; its descendants ship"), + new(OverrideKind.SuppressOperation, Method: null, "/identityproviders", Match: PathMatch.Prefix, Value: null, + Reason: "oracle: the deprecated top-level /identityProviders set ships nothing in v1.0; shipped surface is /identity/identityProviders (Get-MgIdentityProvider)"), ]; [GeneratedRegex(@"\{[^}]*\}")] @@ -107,8 +260,11 @@ private static bool Matches(Entry entry, HttpMethod httpMethod, string normalize // HttpMethod's own equality is case-insensitive, so no string comparison is needed. if (entry.Method is not null && entry.Method != httpMethod) return false; - return entry.ExactPath - ? string.Equals(normalizedPath, entry.PathPrefix, StringComparison.Ordinal) - : normalizedPath.StartsWith(entry.PathPrefix, StringComparison.Ordinal); + return entry.Match switch + { + PathMatch.Exact => string.Equals(normalizedPath, entry.Pattern, StringComparison.Ordinal), + PathMatch.Prefix => normalizedPath.StartsWith(entry.Pattern, StringComparison.Ordinal), + _ => normalizedPath.EndsWith(entry.Pattern, StringComparison.Ordinal), + }; } } diff --git a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs index f339190253b..b0b1edd9235 100644 --- a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs +++ b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs @@ -20,6 +20,12 @@ public sealed partial class PowerShellWrapperGenerationService private readonly GeneratorConfig config; private readonly ILogger logger; private readonly HashSet modelSubNamespaces; + + // Every file written this run, keyed case-insensitively (Windows file systems are), so a + // second cmdlet resolving to an existing file is a detected collision instead of a silent + // overwrite. + private readonly Dictionary writtenCmdletFiles = new(StringComparer.OrdinalIgnoreCase); + private readonly List fileCollisions = []; private readonly Dictionary kiotaReservedRenames; public PowerShellWrapperGenerationService(OpenApiDocument document, GeneratorConfig configuration, ILogger logger) @@ -96,6 +102,9 @@ public async Task GenerateAsync(CancellationToken cancellationToken) var ctx = new EmitContext(ClientNamespace: config.ClientNamespaceName); + writtenCmdletFiles.Clear(); + fileCollisions.Clear(); + Directory.CreateDirectory(config.OutputPath); foreach (var stale in Directory.GetFiles(config.OutputPath, "*.g.cs")) File.Delete(stale); @@ -198,6 +207,15 @@ public async Task GenerateAsync(CancellationToken cancellationToken) written += await EmitGetOperationsAsync(getOperations, ctx, cancellationToken).ConfigureAwait(false); + // All collisions for the run are reported together so one generation surfaces the + // complete list; see edge-cases/naming-edge-cases.md for how each kind is resolved. + if (fileCollisions.Count > 0) + { + throw new InvalidOperationException( + $"{fileCollisions.Count} cmdlet name collision(s): a later operation would overwrite an already-written cmdlet file. " + + $"Resolve each with a NamingOverrides rename or suppression.\n " + string.Join("\n ", fileCollisions)); + } + LogWroteFiles(written + 1, config.OutputPath); } @@ -208,9 +226,9 @@ public async Task GenerateAsync(CancellationToken cancellationToken) // which one to invoke. // // A pairing is only trusted when it is structurally unambiguous: exactly one collection GET - // and one single-entity GET share the noun, and the item's path is the list's path plus one - // trailing id (Users[UserId].Messages -> Users[UserId].Messages[MessageId]). Everything - // else keeps the standalone shape: singleton navs with no list (GET /users/{id}/calendar), + // and one single-entity GET share the noun, and the item's path extends the list's path by + // exactly one id, in either of the shapes Naming.IsListItemPair accepts. Everything else + // keeps the standalone shape: singleton navs with no list (GET /users/{id}/calendar), // list-only endpoints such as delta queries, or an unexpected same-noun collision. private async Task EmitGetOperationsAsync(List getOperations, EmitContext ctx, CancellationToken cancellationToken) { @@ -298,6 +316,15 @@ private async Task EmitGetOperationsAsync(List getOpera private async Task WriteCmdletFileAsync(CmdletNaming naming, string source, CancellationToken cancellationToken) { var fileName = naming.ClassName.Replace("Command", "", StringComparison.Ordinal) + ".g.cs"; + // Both colliding cmdlets usually share the same name, so the builder expression (the + // request path) is what actually identifies which two operations collided. + var cmdletName = $"{naming.VerbName}-{naming.Noun} [{naming.BuilderExpression}]"; + if (writtenCmdletFiles.TryGetValue(fileName, out var existing)) + { + fileCollisions.Add($"{fileName}: '{cmdletName}' collides with already-written '{existing}'"); + return 0; + } + writtenCmdletFiles[fileName] = cmdletName; await File.WriteAllTextAsync(Path.Combine(config.OutputPath, fileName), source, cancellationToken).ConfigureAwait(false); LogWroteCmdletFile(fileName, naming.VerbName, naming.Noun); return 1; diff --git a/tools/WrapperGenerator/README.md b/tools/WrapperGenerator/README.md index caab1d5e93e..eaf37248beb 100644 --- a/tools/WrapperGenerator/README.md +++ b/tools/WrapperGenerator/README.md @@ -52,7 +52,7 @@ Singularization runs per camel-case word (so `termsAndConditions` → `TermAndCo | ends in `ss`/`us`/`is` stays | `Access`, `Status`, `Analysis` | | trailing `s` drops | `Messages` → `Message` | -A few published names aren't algorithmic — they come from hand-written directives in the SDK's module configs. Those live as data in `NamingOverrides.cs`, each with a cited source, rather than as special cases in the naming code. There are three today: suppress `PATCH /users/{id}/calendar` (the SDK ships no such cmdlet), rename `GET /users/{id}/calendar` to `…UserDefaultCalendar`, and strip the `Solution` prefix for most `/solutions/*` nouns (for example, `Get-MgBookingBusiness`, not `Get-MgSolutionBookingBusiness`) while preserving it for known exceptions such as BackupRestore (`Get-MgSolutionBackupRestore`). +A few published names aren't algorithmic, and the spec publishes some routes the SDK never shipped. Both live as data in `NamingOverrides.cs` — renames mirroring the SDK's hand-written AutoRest directives, and suppressions for routes that ship nothing — each entry citing its evidence: the directive when one exists, otherwise the shipped-command inventory. Examples: the `GET /users/{id}/calendar` rename to `…UserDefaultCalendar` (Calendar.md), the `Solution` prefix strip under `/solutions/*` with the BackupRestore exception (Bookings.md), and the self-referential `sites/{id}/sites` rename to `SubSite`/`GroupSubSite` (Sites.md) — without which the sub-sites cmdlets would collide with `Get-MgSite` itself. The generator fails loudly on any such file collision rather than silently overwriting. ## The one subtle part: list + item GET become one cmdlet @@ -126,7 +126,7 @@ The wrappers compile and run only alongside step 1's output. Wiring the two into | `PowerShellWrapperGenerationService.cs` | The orchestrator: walks the paths, pairs list/item GETs, writes the files | | `CmdletNaming.cs` | Verb + noun + the `client.X[Y].Z` request chain | | `Singularizer.cs` | The per-word singularization rules | -| `NamingOverrides.cs` | The three hand-cited name exceptions | +| `NamingOverrides.cs` | Cited rename/suppression data mirroring the shipped SDK surface | | `CmdletEmitter.cs` | The C# templates for each cmdlet shape (the actual code text) | | `SchemaProperties.cs` | Which body properties become `New`/`Update` parameters | | `OperationInfo.cs`, `EmitContext.cs`, `GeneratorConfig.cs` | Small data/config carriers | @@ -157,7 +157,7 @@ dotnet run --project tools/WrapperGenerator -- ` ```powershell # 1. Naming rules pinned to published Microsoft.Graph names dotnet test tools/WrapperGenerator.Tests -# => Passed! - Failed: 0, Passed: 103, Total: 103 +# => Passed! - Failed: 0, Passed: 115, Total: 115 # 2. Parity gate: generate, then check every cmdlet name against Graph's own command inventory .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath diff --git a/tools/WrapperGenerator/edge-cases/naming-edge-cases.md b/tools/WrapperGenerator/edge-cases/naming-edge-cases.md index e5d541e5621..309c64f5e8d 100644 --- a/tools/WrapperGenerator/edge-cases/naming-edge-cases.md +++ b/tools/WrapperGenerator/edge-cases/naming-edge-cases.md @@ -34,7 +34,7 @@ Entry template (keep the field names exact so the file converts cleanly): ``` ## - **Class:** -- **Status:** +- **Status:** — optionally followed by a short parenthetical qualifier - **Evidence:** - **Decision:** - **Migration impact:** @@ -50,6 +50,8 @@ Entry template (keep the field names exact so the file converts cleanly): | operationId preposition truncation | operationid-truncation | structurally-avoided | | `SkypeForBusiness` subject truncation | operationid-truncation | not-yet-reachable | | `Cookies`/`Skus`/`Dns`/`Ios`/`Statistics` quirks | inflection-defect | reproduced-for-parity | +| Self-referential `sites/{id}/sites` → `SubSite` | adjacent-duplicate-segments | handled | +| Route duplicates (spec paths the SDK never shipped) | duplicate-routes | partially-handled | ## Whois truncated to Whoi on the host navigation @@ -143,6 +145,45 @@ Entry template (keep the field names exact so the file converts cleanly): `data`, `delta`, `quota` (Humanizer-specific mistakes this rule engine never makes) and `statistics` (the one that applied here). +## Self-referential paths collide with their parent's cmdlet + +- **Class:** adjacent-duplicate-segments +- **Status:** handled (loud failure + directive-cited renames) +- **Evidence:** singularizing a self-referencing path collapses it onto its parent's noun: + `/sites/{id}/sites` produced `GetMgSite.g.cs`, silently overwriting the get-site-by-id + cmdlet — the same silent-drop failure AutoRest had. Nothing could detect it: writes are + not logged at console level, the summary counts surviving files, and the parity gate only + inspects files that exist. +- **Decision:** the generator now fails generation loudly on any cmdlet file collision, + listing every colliding pair. Shipped cases are renamed via NamingOverrides with their + directive cited (`sites/{id}/sites` → `SubSite`/`GroupSubSite`, per Sites.md + `subject: SubSite` directives); paths the SDK ships nothing for are suppressed as they + surface. +- **Migration impact:** none — renames match the published names exactly. +- **References:** issue #3704; `NamingOverrides.cs` SubSite entries; Sites.md lines 32–61. + +## Route duplicates: the spec publishes paths the SDK never shipped + +- **Class:** duplicate-routes +- **Status:** partially-handled (oracle-cited suppressions/renames) +- **Evidence:** the collision guard's first full-inventory sweep found 966 silent collisions + (per-module counts on issue #3704). Beyond self-references, the dominant cause is the spec + publishing the same data + under two routes while the SDK ships exactly one: nested navs duplicating top-level sets + (`hosts/{id}/components` vs `hostComponents` — 14 Security paths, ships nothing nested), + default-singleton vs collection (`/users/{id}/drive` ships renamed `UserDefaultDrive`; + `/users/{id}/calendar/events` ships `UserDefaultCalendarEvent`), Info-wrapper navs that + never shipped (`pinnedMessages/{id}/message`), and stitched pairs where GET ships from one + route and PATCH/DELETE from the other (termStore, agreement file/files). +- **Decision:** each resolved family is a `NamingOverrides` entry citing the shipped + command or the oracle's absence. Two families remain open on #3704 with full evidence: + the Identity.Governance mirrored navigations (the shipped survivor alternates by nesting + level, needing a dedupe design decision) and the Sites termStore `children` recursion + (resolver and direct oracle probes disagree; needs reconciliation before encoding). +- **Migration impact:** none — suppressed routes never shipped; renames match shipped names. +- **References:** issue #3704 (remainder inventory + resolver evidence); `NamingOverrides.cs` + "Collision resolutions" section. + ## Watch list Cases spotted but deliberately not acted on yet, so they aren't lost: From e6c98e4a78156aa91a3f22fee514aa5c24deb73a Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Wed, 12 Aug 2026 10:12:12 -0700 Subject: [PATCH 08/13] feat(wrapper-generator): resolve cmdlet collisions via oracle-derived data Derive-CollisionResolutions.ps1 replays the checked-in collision inventory (212 lines, 365 contested routes) against MgCommandMetadata and emits exact-match resolution data: 191 suppressions (routes the published SDK prunes) and 64 renames (published nouns), each entry carrying its oracle evidence. The files embed into the generator and apply only when UseCollisionData is set; -Validate fails on drift, and a new xunit test runs it on every `dotnet test` so staleness fails the suite instead of depending on someone remembering to run the script by hand. Derivation itself fails on any unclassified or ambiguous route. Only 2 cross-path variant merges exist in all of v1.0 (GroupPhoto, ShareListItem) - deferred with the singleton side kept, cataloged in crosspath-merge-edge-cases.md. Full 39-module v1.0 generation now produces zero collisions; 20 published commands that lost filename races are recovered; exact-name matches rise 5,042 -> 5,098. Also: cmdlets emit into a per-module namespace derived from the client namespace instead of the leftover MgPoC placeholder; Build-WrapperModule's generated csproj references Authentication by a relative path instead of an absolute one; its -Configuration parameter now actually reaches the wrapper generator's own build, not just the final module build; and a pre-existing nullable warning in the list/item pairing check is fixed. 121 tests pass. --- tools/Build-WrapperModule.ps1 | 10 +- tools/Derive-CollisionResolutions.ps1 | 239 ++ .../CollisionDataDriftTests.cs | 49 + .../DerivedCollisionResolutionsTests.cs | 60 + tools/WrapperGenerator/CmdletEmitter.cs | 4 +- tools/WrapperGenerator/CmdletNaming.cs | 6 +- .../DerivedCollisionResolutions.cs | 75 + tools/WrapperGenerator/EmitContext.cs | 14 +- tools/WrapperGenerator/GeneratorConfig.cs | 14 +- tools/WrapperGenerator/NamingOverrides.cs | 13 +- .../PowerShellWrapperGenerationService.cs | 4 +- tools/WrapperGenerator/Program.cs | 15 +- tools/WrapperGenerator/README.md | 8 +- .../WrapperGenerator/WrapperGenerator.csproj | 6 + .../data/collision-inventory.v1.0.txt | 212 + .../data/collision-renames.v1.0.json | 1254 ++++++ .../data/collision-resolution-ledger.v1.0.csv | 366 ++ .../data/collision-suppressions.v1.0.json | 3792 +++++++++++++++++ .../edge-cases/crosspath-merge-edge-cases.md | 49 + 19 files changed, 6167 insertions(+), 23 deletions(-) create mode 100644 tools/Derive-CollisionResolutions.ps1 create mode 100644 tools/WrapperGenerator.Tests/CollisionDataDriftTests.cs create mode 100644 tools/WrapperGenerator.Tests/DerivedCollisionResolutionsTests.cs create mode 100644 tools/WrapperGenerator/DerivedCollisionResolutions.cs create mode 100644 tools/WrapperGenerator/data/collision-inventory.v1.0.txt create mode 100644 tools/WrapperGenerator/data/collision-renames.v1.0.json create mode 100644 tools/WrapperGenerator/data/collision-resolution-ledger.v1.0.csv create mode 100644 tools/WrapperGenerator/data/collision-suppressions.v1.0.json create mode 100644 tools/WrapperGenerator/edge-cases/crosspath-merge-edge-cases.md diff --git a/tools/Build-WrapperModule.ps1 b/tools/Build-WrapperModule.ps1 index 3a5a392c1dd..7630a2563ef 100644 --- a/tools/Build-WrapperModule.ps1 +++ b/tools/Build-WrapperModule.ps1 @@ -142,7 +142,7 @@ function Build-OneModule { } } - $wrapperOut = & dotnet run --project $generatorProject -- -d $spec -o $cmdletsDir -n $clientNs 2>&1 + $wrapperOut = & dotnet run --project $generatorProject -c $Configuration -- -d $spec -o $cmdletsDir -n $clientNs --api-version $ApiVersion 2>&1 if ($LASTEXITCODE -ne 0) { # Skip warnings precede the failure; the exception message is what identifies it. $result.FailedAt = 'wrapper-generator' @@ -157,8 +157,12 @@ function Build-OneModule { return $result } - # Generated artifact, machine-local by design (absolute reference into this clone). + # Relative to $srcDir rather than the absolute $authCsproj, so the csproj is portable + # across clones and stays correct if a module's output folder ever moves (the eventual + # src/// commit target sits at a different depth than + # artifacts/wrapper-modules//src/). $csprojPath = Join-Path $srcDir "$moduleName.csproj" + $authCsprojRelative = [System.IO.Path]::GetRelativePath($srcDir, $authCsproj) -replace '/', '\' @" @@ -180,7 +184,7 @@ function Build-OneModule { - + diff --git a/tools/Derive-CollisionResolutions.ps1 b/tools/Derive-CollisionResolutions.ps1 new file mode 100644 index 00000000000..6171f01fbb7 --- /dev/null +++ b/tools/Derive-CollisionResolutions.ps1 @@ -0,0 +1,239 @@ +<# +.SYNOPSIS +Derives the wrapper generator's collision-resolution data files from the published-command +oracle, and validates the checked-in files against a fresh derivation. + +.DESCRIPTION +Input is a collision inventory: the exact lines the generator prints when it fails loudly on +cmdlet file collisions (one "Module :: File: 'Verb-Noun [Builder]' collides with +already-written 'Verb-Noun [Builder]'" per line), captured from a generation run with no +collision resolutions applied. + +For every route that appears in the inventory, the script asks the oracle +(MgCommandMetadata.json, filtered to -ApiVersion) what the published SDK ships for that +method + URI, and derives exactly one action: + + keep the route ships under the same name the generator produces - no entry emitted + suppress the route ships nothing - the published SDK pruned it + rename the route ships under a different noun - entry carries the published noun + +Anything else is a hard failure: + - an inventory line that does not parse, + - the same route deriving two different actions from different lines, + - a cross-path merge (both routes ship the SAME command from DIFFERENT URIs - the + generator cannot represent that yet) that no curated NamingOverrides entry resolves. + +Output is two deterministic JSON files (sorted, no timestamps) so renames review separately +from suppressions: + + tools/WrapperGenerator/data/collision-suppressions..json + tools/WrapperGenerator/data/collision-renames..json + +plus an operation-level ledger of every inventory route -> action -> evidence: + + /collision-resolution-ledger..csv + +.PARAMETER Validate +Re-derive and byte-compare against the checked-in data files instead of writing them. +Exits 1 on any difference, so drift between oracle, inventory, and data cannot land silently. + +.EXAMPLE +.\tools\Derive-CollisionResolutions.ps1 +.EXAMPLE +.\tools\Derive-CollisionResolutions.ps1 -Validate +#> +[CmdletBinding()] +param( + # The checked-in inventory snapshot: every collision line from a full-module generation + # run with the derived data disabled (WrapperGenerator --no-collision-data). Re-capture it + # with that flag whenever specs or naming rules change, then re-derive. + [string]$InventoryPath, + [string]$OraclePath = "$PSScriptRoot\..\src\Authentication\Authentication\custom\common\MgCommandMetadata.json", + [string]$OutDir = "$PSScriptRoot\WrapperGenerator\data", + [string]$LedgerPath, + [ValidateSet('v1.0', 'beta')] + [string]$ApiVersion = 'v1.0', + [switch]$Validate +) +if (-not $InventoryPath) { $InventoryPath = "$PSScriptRoot\WrapperGenerator\data\collision-inventory.$ApiVersion.txt" } +# Checked in alongside the inventory (NOT artifacts/, which is gitignored) so it ships as +# reviewable evidence in the PR diff. It is regenerated on every run but NOT compared by +# -Validate — only the two collision-*.json files are the enforced contract; this CSV is the +# human-readable "why" behind them, kept in sync by convention, not by the drift gate. +if (-not $LedgerPath) { $LedgerPath = "$PSScriptRoot\WrapperGenerator\data\collision-resolution-ledger.$ApiVersion.csv" } + +$ErrorActionPreference = 'Stop' + +# ---- parse the inventory ------------------------------------------------------------------- +$lineRx = "^(?[^:]+?) :: (?\S+): '(?[A-Za-z]+)-(?[A-Za-z0-9]+) \[(?[^\]]*(?:\[[^\]]*\][^\]]*)*)\]' collides with already-written '(?[A-Za-z]+)-(?[A-Za-z0-9]+) \[(?[^\]]*(?:\[[^\]]*\][^\]]*)*)\]'$" +$verbToMethod = @{ Get = 'GET'; New = 'POST'; Update = 'PATCH'; Set = 'PUT'; Remove = 'DELETE' } + +# Builder expression -> the same normalized URI skeleton NamingOverrides.NormalizePath +# produces from a path template: lowercase fixed segments, every parameter erased to {}. +function ConvertTo-UriSkeleton([string]$builder) { + $parts = @() + foreach ($seg in ($builder -split '\.')) { + if ($seg -notmatch '^(?[A-Za-z0-9]+)(\[(?[^\]]+)\])?$') { return $null } + $parts += $Matches.n.ToLowerInvariant() + if ($Matches.i) { $parts += '{}' } + } + '/' + ($parts -join '/') +} + +$lines = @(Get-Content $InventoryPath | Where-Object { $_.Trim() }) +$parsed = @() +$unparsed = @() +foreach ($l in $lines) { + if ($l -match $lineRx) { + $lost = ConvertTo-UriSkeleton $Matches.lost + $kept = ConvertTo-UriSkeleton $Matches.kept + if (-not $lost -or -not $kept) { $unparsed += $l; continue } + $parsed += [pscustomobject]@{ + Module = $Matches.mod.Trim(); Method = $verbToMethod[$Matches.verb] + OurName = "$($Matches.verb)-$($Matches.noun)"; Lost = $lost; Kept = $kept + } + } + else { $unparsed += $l } +} +if ($unparsed) { + $unparsed | ForEach-Object { Write-Error -ErrorAction Continue "unparsed inventory line: $_" } + throw "$($unparsed.Count) inventory line(s) did not parse; refusing to derive from a partial inventory." +} +Write-Host "inventory: $($parsed.Count) collision lines" + +# ---- oracle lookup: METHOD + skeleton -> published commands -------------------------------- +$oracle = @{} +foreach ($e in (Get-Content $OraclePath -Raw | ConvertFrom-Json)) { + if ($e.ApiVersion -ne $ApiVersion) { continue } + $skel = (($e.Uri -split '/') | ForEach-Object { if ($_ -match '^\{') { '{}' } else { $_.ToLowerInvariant() } }) -join '/' + $k = "$($e.Method) $skel" + if (-not $oracle.ContainsKey($k)) { $oracle[$k] = [System.Collections.Generic.SortedSet[string]]::new() } + [void]$oracle[$k].Add($e.Command) +} + +# ---- derive one action per route ------------------------------------------------------------ +# Route identity is (method, skeleton). Every inventory line contributes both of its routes. +$routes = @{} +function Add-Route($module, $method, $skel, $ourName, $counterpartSkel) { + $key = "$method $skel" + if (-not $routes.ContainsKey($key)) { + $routes[$key] = [pscustomobject]@{ + Method = $method; Uri = $skel; OurName = $ourName + Modules = [System.Collections.Generic.SortedSet[string]]::new() + Counterparts = [System.Collections.Generic.SortedSet[string]]::new() + } + } + $r = $routes[$key] + if ($r.OurName -cne $ourName) { + throw "ambiguous: route '$key' produces both '$($r.OurName)' and '$ourName' in the inventory." + } + [void]$r.Modules.Add($module) + [void]$r.Counterparts.Add($counterpartSkel) +} +foreach ($p in $parsed) { + Add-Route $p.Module $p.Method $p.Lost $p.OurName $p.Kept + Add-Route $p.Module $p.Method $p.Kept $p.OurName $p.Lost +} +Write-Host "routes contested: $($routes.Count)" + +# Pass 1 - tentative action per route, straight from the oracle: +# ships nothing -> suppress +# ships under our name -> keep (subject to the cross-path pass below) +# ships renamed -> rename to the published noun +$failures = @() +foreach ($key in ($routes.Keys | Sort-Object)) { + $r = $routes[$key] + $ships = if ($oracle.ContainsKey($key)) { @($oracle[$key]) } else { @() } + # The comma operator keeps a single-element array an array through Add-Member's binder. + $r | Add-Member ShipsAs (, $ships) + $action = + if ($ships.Count -eq 0) { 'suppress' } + elseif ($ships -ccontains $r.OurName) { 'keep' } + else { + $shippedNouns = @($ships | ForEach-Object { ($_ -split '-', 2)[1] -replace '^Mg', '' } | Sort-Object -Unique) + if ($shippedNouns.Count -ne 1) { + $failures += "ambiguous rename: $key ships as [$($ships -join ', ')] - more than one target noun." + } + 'rename' + } + $r | Add-Member Action $action +} + +# Pass 2 - cross-path merges. The published SDK serves ONE command from several URIs as +# parameter-set variants (Get-MgSiteTermStoreSetChild covers /children, /children/{}, +# /children/{}/children, /children/{}/children/{}). The wrapper cannot express that yet, so +# among same-command keep-routes only the shallowest list/item pair survives: the route with +# the fewest path parameters (tie: shortest, then ordinal - fully deterministic) plus its +# trailing-id partner. Deeper twins are suppressed and marked deferred; they come back when +# cross-path parameter sets land (tracked in the operation-shapes issue). +foreach ($group in ($routes.Values | Where-Object { $_.Action -eq 'keep' } | + Group-Object { "$($_.Method) $($_.OurName)" } | Where-Object Count -gt 1)) { + $anchor = $group.Group | Sort-Object ` + @{e = { ([regex]::Matches($_.Uri, '\{\}')).Count } }, @{e = { $_.Uri.Length } }, @{e = { $_.Uri } } | + Select-Object -First 1 + foreach ($r in $group.Group) { + if ($r.Uri -cne $anchor.Uri -and $r.Uri -cne "$($anchor.Uri)/{}") { $r.Action = 'suppress-deferred' } + } +} + +if ($failures) { + $failures | ForEach-Object { Write-Error -ErrorAction Continue $_ } + throw "$($failures.Count) route(s) unclassified or ambiguous; refusing to emit a partial derivation." +} + +$suppressions = @(); $renames = @(); $ledger = @() +foreach ($key in ($routes.Keys | Sort-Object)) { + $r = $routes[$key] + $counterpartShips = @($r.Counterparts | ForEach-Object { $ck = "$($r.Method) $_" + if ($oracle.ContainsKey($ck)) { @($oracle[$ck]) } else { @() } } | Sort-Object -Unique) + $entry = [ordered]@{ + apiVersion = $ApiVersion; modules = @($r.Modules); method = $r.Method; uri = $r.Uri + action = if ($r.Action -eq 'suppress-deferred') { 'suppress' } else { $r.Action } + evidence = [ordered]@{ + shipsAs = @($r.ShipsAs); counterpartUris = @($r.Counterparts); counterpartShipsAs = $counterpartShips + } + } + if ($r.Action -eq 'suppress-deferred') { $entry.deferredCrossPathMerge = $true } + if ($r.Action -eq 'rename') { $entry.replacementNoun = (@($r.ShipsAs)[0] -split '-', 2)[1] -replace '^Mg', '' } + switch ($entry.action) { + 'suppress' { $suppressions += [pscustomobject]$entry } + 'rename' { $renames += [pscustomobject]$entry } + } + $ledger += [pscustomobject]@{ + Method = $r.Method; Uri = $r.Uri; Modules = ($r.Modules -join ';'); OurName = $r.OurName + Action = $r.Action; ShipsAs = ($r.ShipsAs -join ';'); CounterpartUris = ($r.Counterparts -join ';') + CounterpartShipsAs = ($counterpartShips -join ';') + } +} + +# ---- write or validate ---------------------------------------------------------------------- +$jsonOpts = [System.Text.Json.JsonSerializerOptions]::new() +$jsonOpts.WriteIndented = $true +function ToJson($obj) { + # ConvertTo-Json reorders nothing, but normalize newlines so the byte-compare is stable. + (($obj | ConvertTo-Json -Depth 6) -replace "`r`n", "`n") + "`n" +} +$targets = @( + @{ Path = Join-Path $OutDir "collision-suppressions.$ApiVersion.json"; Content = ToJson $suppressions }, + @{ Path = Join-Path $OutDir "collision-renames.$ApiVersion.json"; Content = ToJson $renames } +) +if ($Validate) { + $drift = @() + foreach ($t in $targets) { + if (-not (Test-Path $t.Path)) { $drift += "missing: $($t.Path)"; continue } + $existing = (Get-Content $t.Path -Raw) -replace "`r`n", "`n" + if ($existing -cne $t.Content) { $drift += "differs from fresh derivation: $($t.Path)" } + } + if ($drift) { + $drift | ForEach-Object { Write-Error -ErrorAction Continue $_ } + exit 1 + } + Write-Host "validation OK: $($suppressions.Count) suppressions + $($renames.Count) renames match the checked-in files." + exit 0 +} + +New-Item -ItemType Directory -Force $OutDir | Out-Null +foreach ($t in $targets) { [System.IO.File]::WriteAllText($t.Path, $t.Content) } +New-Item -ItemType Directory -Force (Split-Path $LedgerPath) | Out-Null +$ledger | Sort-Object Method, Uri | Export-Csv $LedgerPath -NoTypeInformation +Write-Host "wrote $($suppressions.Count) suppressions, $($renames.Count) renames, ledger of $($ledger.Count) routes -> $LedgerPath" diff --git a/tools/WrapperGenerator.Tests/CollisionDataDriftTests.cs b/tools/WrapperGenerator.Tests/CollisionDataDriftTests.cs new file mode 100644 index 00000000000..b539877d3d3 --- /dev/null +++ b/tools/WrapperGenerator.Tests/CollisionDataDriftTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using System.IO; +using Xunit; + +namespace WrapperGenerator.Tests; + +// The checked-in collision-resolution data (tools/WrapperGenerator/data/collision-*.json) is +// derived FROM tools/WrapperGenerator/data/collision-inventory.v1.0.txt and the oracle +// (MgCommandMetadata.json) by tools/Derive-CollisionResolutions.ps1. Nothing else enforces +// that the checked-in files still match a fresh derivation — there is no CI pipeline for this +// project yet (tracked separately) — so this test is the drift gate: it shells out to the +// script's -Validate mode as part of the normal `dotnet test` run, the same command a human +// would run by hand, so staleness fails the suite instead of depending on someone remembering +// to run it. +public sealed class CollisionDataDriftTests +{ + [Fact] + public void DerivedCollisionDataMatchesAFreshDerivation() + { + var scriptPath = Path.Combine(FindRepoRoot(), "tools", "Derive-CollisionResolutions.ps1"); + Assert.True(File.Exists(scriptPath), $"Derivation script not found at '{scriptPath}'."); + + var psi = new ProcessStartInfo("pwsh") + { + ArgumentList = { "-NoProfile", "-NonInteractive", "-File", scriptPath, "-Validate" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + using var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start pwsh."); + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + Assert.True(process.ExitCode == 0, + "Checked-in collision-suppressions/renames JSON no longer matches a fresh derivation from " + + "collision-inventory.v1.0.txt and the oracle. Re-run tools/Derive-CollisionResolutions.ps1 " + + $"(without -Validate) and commit the result.\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"); + } + + private static string FindRepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, ".git"))) + dir = dir.Parent; + return dir?.FullName ?? throw new InvalidOperationException("Could not locate repo root (.git) from " + AppContext.BaseDirectory); + } +} diff --git a/tools/WrapperGenerator.Tests/DerivedCollisionResolutionsTests.cs b/tools/WrapperGenerator.Tests/DerivedCollisionResolutionsTests.cs new file mode 100644 index 00000000000..da3a2541194 --- /dev/null +++ b/tools/WrapperGenerator.Tests/DerivedCollisionResolutionsTests.cs @@ -0,0 +1,60 @@ +using System.Net.Http; +using WrapperGenerator; +using Xunit; + +namespace WrapperGenerator.Tests; + +// The derived collision data (tools/WrapperGenerator/data/collision-*.json, embedded at +// build time) must only act when a run opts in via GeneratorConfig: the curated-only paths +// the naming tests pin are exercised with config null, so a data-file regeneration can never +// silently shift those expectations. Entries asserted here are oracle-cited in the data +// files' evidence fields. +public sealed class DerivedCollisionResolutionsTests +{ + private static readonly GeneratorConfig DataOn = new("Test.Client", "unused"); + private static readonly GeneratorConfig DataOff = new("Test.Client", "unused", UseCollisionData: false); + + // Oracle: /groupSettings ships nothing in v1.0; Get/New/Update/Remove-MgGroupSetting all + // ship from the nested /groups/{id}/settings routes. + [Fact] + public void DerivedSuppressionAppliesOnlyWithDataEnabled() + { + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, "/groupSettings", DataOn)); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/groupSettings", DataOff)); + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/groupSettings")); + } + + // Oracle: the nested catalog resourceRoles route ships as + // Get-MgEntitlementManagementCatalogResourceRole - the published noun drops the + // IdentityGovernance prefix our path rules produce. + [Fact] + public void DerivedRenameReplacesTheNounVerbatim() + { + var path = "/identityGovernance/entitlementManagement/catalogs/{accessPackageCatalog-id}/resourceRoles"; + + var renamed = Naming.Resolve(new OperationInfo(HttpMethod.Get, path), DataOn); + Assert.Equal("MgEntitlementManagementCatalogResourceRole", renamed.Noun); + + var untouched = Naming.Resolve(new OperationInfo(HttpMethod.Get, path)); + Assert.Equal("MgIdentityGovernanceEntitlementManagementCatalogResourceRole", untouched.Noun); + } + + // A derived rename is keyed by method: the GET rename of the resourceRoles route must not + // leak onto a POST of a DIFFERENT route that only shares the prefix. + [Fact] + public void DerivedEntriesAreExactMatchOnly() + { + Assert.False(NamingOverrides.IsSuppressed(HttpMethod.Get, "/groupSettings/extra/segment", DataOn)); + } + + // The two deferred cross-path merges (the only ones in all of v1.0): the published SDK + // serves one command from two unrelated routes; the singleton side is kept, the + // collection side is suppressed until cross-path parameter sets land. + [Theory] + [InlineData("/groups/{group-id}/photos")] + [InlineData("/shares/{sharedDriveItem-id}/list/items")] + public void DeferredCrossPathRoutesAreSuppressed(string pathTemplate) + { + Assert.True(NamingOverrides.IsSuppressed(HttpMethod.Get, pathTemplate, DataOn)); + } +} diff --git a/tools/WrapperGenerator/CmdletEmitter.cs b/tools/WrapperGenerator/CmdletEmitter.cs index f88ae8742b0..985309edfe6 100644 --- a/tools/WrapperGenerator/CmdletEmitter.cs +++ b/tools/WrapperGenerator/CmdletEmitter.cs @@ -25,7 +25,7 @@ public static class CmdletEmitter } else { - WriteVerbose("[MgPoC] No -AccessToken supplied, using the active Connect-MgGraph session."); + WriteVerbose("No -AccessToken supplied, using the active Connect-MgGraph session."); try { httpClient = HttpHelpers.GetGraphHttpClient(); @@ -667,7 +667,7 @@ private static string ReFetchBlock(CmdletNaming naming) => $$""" if (result is null) { - WriteVerbose("[MgPoC] PATCH succeeded with no response body, re-fetching the updated resource."); + WriteVerbose("PATCH succeeded with no response body, re-fetching the updated resource."); try { result = client.{{naming.BuilderExpression}}.GetAsync().GetAwaiter().GetResult(); diff --git a/tools/WrapperGenerator/CmdletNaming.cs b/tools/WrapperGenerator/CmdletNaming.cs index a9836723e2e..0ee3046c5a1 100644 --- a/tools/WrapperGenerator/CmdletNaming.cs +++ b/tools/WrapperGenerator/CmdletNaming.cs @@ -41,7 +41,7 @@ public static class Naming [HttpMethod.Delete] = PsVerb.Remove, }; - public static CmdletNaming Resolve(OperationInfo operation) + public static CmdletNaming Resolve(OperationInfo operation, GeneratorConfig? config = null) { ArgumentNullException.ThrowIfNull(operation); if (!VerbMap.TryGetValue(operation.HttpMethod, out var verb)) @@ -51,7 +51,7 @@ public static CmdletNaming Resolve(OperationInfo operation) // plurality the spec author chose, while the published SDK names follow the path: // GET /users/{id}/messages is Get-MgUserMessage. The few hand-tuned exceptions the // published SDK carries are mirrored as data in NamingOverrides, never as code here. - var noun = GeneratorConstants.NounPrefix + NamingOverrides.ApplyNounOverrides(operation.HttpMethod, operation.Path, BuildNounFromPath(operation.Path)); + var noun = GeneratorConstants.NounPrefix + NamingOverrides.ApplyNounOverrides(operation.HttpMethod, operation.Path, BuildNounFromPath(operation.Path), config); // A list GET (/users/{id}/messages) and its item GET (/users/{id}/messages/{message-id}) // get the same noun on purpose. PowerShellWrapperGenerationService pairs them into one @@ -219,7 +219,7 @@ public static bool IsListItemPair(CmdletNaming list, CmdletNaming item) // plain list/item pair; without this the two emit identical file names and collide. var listCast = TrailingCastMember(list.BuilderExpression); var itemCast = TrailingCastMember(item.BuilderExpression); - if (listCast is null || !string.Equals(listCast, itemCast, StringComparison.Ordinal)) + if (listCast is null || itemCast is null || !string.Equals(listCast, itemCast, StringComparison.Ordinal)) return false; var listStem = list.BuilderExpression[..^(listCast.Length + 1)]; diff --git a/tools/WrapperGenerator/DerivedCollisionResolutions.cs b/tools/WrapperGenerator/DerivedCollisionResolutions.cs new file mode 100644 index 00000000000..793377e8c69 --- /dev/null +++ b/tools/WrapperGenerator/DerivedCollisionResolutions.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; + +namespace WrapperGenerator; + +// Collision resolutions DERIVED from the published-command oracle, as opposed to the curated +// judgment entries in NamingOverrides. tools/Derive-CollisionResolutions.ps1 writes the +// data/collision-*.json files from (collision inventory x MgCommandMetadata.json) and its +// -Validate mode fails when the checked-in files drift from a fresh derivation; the files are +// embedded at build time so a generation run never reads the 22 MB oracle itself. +// +// Entries are exact-match only, keyed by API version + HTTP method + normalized URI, and +// exist solely for operations that appeared in the collision inventory. Anything broader +// (subtree prunes, cross-path merge picks) is curated in NamingOverrides with a citation. +internal static class DerivedCollisionResolutions +{ + private sealed record DataEntry(string ApiVersion, string Method, string Uri, string Action, string? ReplacementNoun); + + private sealed record Tables(HashSet Suppressions, Dictionary Renames); + + private static readonly Lazy> ByApiVersion = new(Load); + + private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; + + public static bool IsSuppressed(string apiVersion, HttpMethod method, string normalizedPath) => + ByApiVersion.Value.TryGetValue(apiVersion, out var tables) + && tables.Suppressions.Contains(Key(method, normalizedPath)); + + public static bool TryReplaceNoun(string apiVersion, HttpMethod method, string normalizedPath, out string noun) + { + noun = string.Empty; + return ByApiVersion.Value.TryGetValue(apiVersion, out var tables) + && tables.Renames.TryGetValue(Key(method, normalizedPath), out noun!); + } + + private static string Key(HttpMethod method, string normalizedPath) => $"{method.Method.ToUpperInvariant()} {normalizedPath}"; + + private static Dictionary Load() + { + var result = new Dictionary(StringComparer.Ordinal); + var assembly = typeof(DerivedCollisionResolutions).Assembly; + foreach (var resource in assembly.GetManifestResourceNames()) + { + if (!resource.Contains(".data.collision-", StringComparison.Ordinal) || !resource.EndsWith(".json", StringComparison.Ordinal)) + continue; + using var stream = assembly.GetManifestResourceStream(resource)!; + var entries = JsonSerializer.Deserialize>(stream, JsonOptions) ?? []; + foreach (var entry in entries) + { + if (!result.TryGetValue(entry.ApiVersion, out var tables)) + result[entry.ApiVersion] = tables = new Tables(new HashSet(StringComparer.Ordinal), new Dictionary(StringComparer.Ordinal)); + var key = $"{entry.Method.ToUpperInvariant()} {entry.Uri}"; + switch (entry.Action) + { + case "suppress": + tables.Suppressions.Add(key); + break; + case "rename" when !string.IsNullOrEmpty(entry.ReplacementNoun): + tables.Renames[key] = entry.ReplacementNoun; + break; + default: + // A malformed data file must fail the run, not silently generate the + // very collision the entry was derived to resolve. + throw new InvalidDataException($"{resource}: entry '{key}' has unsupported action '{entry.Action}'."); + } + } + } + return result; + } +} diff --git a/tools/WrapperGenerator/EmitContext.cs b/tools/WrapperGenerator/EmitContext.cs index 9a738aa7f90..f2db739ffa4 100644 --- a/tools/WrapperGenerator/EmitContext.cs +++ b/tools/WrapperGenerator/EmitContext.cs @@ -1,9 +1,19 @@ -namespace WrapperGenerator; +using System; + +namespace WrapperGenerator; // The per-module values CmdletEmitter's templates need, so the emitter stays module-agnostic. // ClientNamespace is whatever --namespace-name the module was generated with, for example // "Microsoft.Graph.PowerShell.Mail.Client". -public sealed record EmitContext(string ClientNamespace, string CmdletNamespace = "MgPoC") +public sealed record EmitContext(string ClientNamespace) { public string ModelsNamespace => $"{ClientNamespace}.Models"; + + // The emitted cmdlets' own namespace: the client namespace with its trailing ".Client" + // dropped ("Microsoft.Graph.PowerShell.Mail.Client" -> "Microsoft.Graph.PowerShell.Mail"), + // so it is per-module like everything else the client generates rather than a shared + // placeholder every module's cmdlets would otherwise collide into. + public string CmdletNamespace => ClientNamespace.EndsWith(".Client", StringComparison.Ordinal) + ? ClientNamespace[..^".Client".Length] + : ClientNamespace; } diff --git a/tools/WrapperGenerator/GeneratorConfig.cs b/tools/WrapperGenerator/GeneratorConfig.cs index 0b8f90c7967..dc4d64f57f4 100644 --- a/tools/WrapperGenerator/GeneratorConfig.cs +++ b/tools/WrapperGenerator/GeneratorConfig.cs @@ -1,5 +1,11 @@ -namespace WrapperGenerator; +namespace WrapperGenerator; -// Configuration for a generation run. The generation service reads exactly two values: the -// client namespace the module is generated with, and the output folder for the .g.cs files. -public sealed record GeneratorConfig(string ClientNamespaceName, string OutputPath); +// Configuration for a generation run: the client namespace the module is generated with, the +// output folder for the .g.cs files, the API version the derived collision-resolution data is +// keyed by, and whether that data is applied at all (derivation runs disable it to reproduce +// the raw collision inventory the data is derived FROM). +public sealed record GeneratorConfig( + string ClientNamespaceName, + string OutputPath, + string ApiVersion = "v1.0", + bool UseCollisionData = true); diff --git a/tools/WrapperGenerator/NamingOverrides.cs b/tools/WrapperGenerator/NamingOverrides.cs index 3aae42bfc8d..f7972a4364a 100644 --- a/tools/WrapperGenerator/NamingOverrides.cs +++ b/tools/WrapperGenerator/NamingOverrides.cs @@ -213,11 +213,16 @@ private sealed record Entry(OverrideKind Kind, HttpMethod? Method, string Patter private static string NormalizePath(string pathTemplate) => PathParamRegex().Replace(pathTemplate, "{}").TrimEnd('/').ToLowerInvariant(); - public static bool IsSuppressed(HttpMethod httpMethod, string pathTemplate) + // config carries the API version the derived collision data is keyed by; null (the unit + // tests' default) applies only the curated entries below, so a data-file change can never + // silently shift a pinned naming expectation. + public static bool IsSuppressed(HttpMethod httpMethod, string pathTemplate, GeneratorConfig? config = null) { ArgumentNullException.ThrowIfNull(httpMethod); ArgumentNullException.ThrowIfNull(pathTemplate); var path = NormalizePath(pathTemplate); + if (config is { UseCollisionData: true } && DerivedCollisionResolutions.IsSuppressed(config.ApiVersion, httpMethod, path)) + return true; foreach (var entry in Entries) { if (entry.Kind == OverrideKind.SuppressOperation && Matches(entry, httpMethod, path)) @@ -226,13 +231,17 @@ public static bool IsSuppressed(HttpMethod httpMethod, string pathTemplate) return false; } - public static string ApplyNounOverrides(HttpMethod httpMethod, string pathTemplate, string noun) + public static string ApplyNounOverrides(HttpMethod httpMethod, string pathTemplate, string noun, GeneratorConfig? config = null) { ArgumentNullException.ThrowIfNull(httpMethod); ArgumentNullException.ThrowIfNull(pathTemplate); ArgumentNullException.ThrowIfNull(noun); var path = NormalizePath(pathTemplate); + // A derived rename is the published noun verbatim; nothing curated may rewrite it. + if (config is { UseCollisionData: true } && DerivedCollisionResolutions.TryReplaceNoun(config.ApiVersion, httpMethod, path, out var derivedNoun)) + return derivedNoun; + // Published BackupRestore cmdlets retain the Solution prefix (for example, // Get-MgSolutionBackupRestore). Do not apply /solutions/* strip rules here. var skipSolutionStrip = path.StartsWith("/solutions/backuprestore", StringComparison.Ordinal); diff --git a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs index b0b1edd9235..82974e0db3e 100644 --- a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs +++ b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs @@ -135,7 +135,7 @@ public async Task GenerateAsync(CancellationToken cancellationToken) // Skip operations the published SDK deliberately does not ship. NamingOverrides // holds the citation for each one. - if (NamingOverrides.IsSuppressed(httpMethod, pathTemplate)) + if (NamingOverrides.IsSuppressed(httpMethod, pathTemplate, config)) { LogSuppressedOperation(httpMethod.Method, pathTemplate); continue; @@ -168,7 +168,7 @@ public async Task GenerateAsync(CancellationToken cancellationToken) : null; var collectionValueSchema = responseSchema is not null ? FindProperty(responseSchema, "value") : null; - var cmdletNaming = Naming.Resolve(new OperationInfo(httpMethod, pathTemplate, headerParams)); + var cmdletNaming = Naming.Resolve(new OperationInfo(httpMethod, pathTemplate, headerParams), config); if (httpMethod == HttpMethod.Get && responseSchema is null) { diff --git a/tools/WrapperGenerator/Program.cs b/tools/WrapperGenerator/Program.cs index dfe08a5a09c..15d43b5dffa 100644 --- a/tools/WrapperGenerator/Program.cs +++ b/tools/WrapperGenerator/Program.cs @@ -20,6 +20,8 @@ private static async Task Main(string[] args) string? specPath = null; string? outputPath = null; string? clientNamespace = null; + var apiVersion = "v1.0"; + var useCollisionData = true; var includePaths = new List(); for (var i = 0; i < args.Length; i++) @@ -35,6 +37,14 @@ private static async Task Main(string[] args) case "-n" or "--namespace" or "--namespace-name": clientNamespace = ArgValue(args, ref i); break; + case "--api-version": + apiVersion = ArgValue(args, ref i); + break; + case "--no-collision-data": + // Derivation mode: tools/Derive-CollisionResolutions.ps1 needs the raw + // collision inventory, so the derived resolutions must not mask it. + useCollisionData = false; + break; case "--include-path": includePaths.Add(ArgValue(args, ref i)); break; @@ -52,7 +62,7 @@ private static async Task Main(string[] args) if (specPath is null || outputPath is null || clientNamespace is null) { Console.Error.WriteLine( - "Usage: WrapperGenerator -d -o -n [--include-path '#GET,POST' ...]"); + "Usage: WrapperGenerator -d -o -n [--api-version v1.0|beta] [--no-collision-data] [--include-path '#GET,POST' ...]"); return 2; } @@ -66,7 +76,8 @@ private static async Task Main(string[] args) IncludePathFilter.Apply(document, includePaths); - var config = new GeneratorConfig(ClientNamespaceName: clientNamespace, OutputPath: outputPath); + var config = new GeneratorConfig(ClientNamespaceName: clientNamespace, OutputPath: outputPath, + ApiVersion: apiVersion, UseCollisionData: useCollisionData); var service = new PowerShellWrapperGenerationService(document, config, new StderrLogger()); await service.GenerateAsync(CancellationToken.None).ConfigureAwait(false); diff --git a/tools/WrapperGenerator/README.md b/tools/WrapperGenerator/README.md index eaf37248beb..60e4aa796a7 100644 --- a/tools/WrapperGenerator/README.md +++ b/tools/WrapperGenerator/README.md @@ -54,6 +54,8 @@ Singularization runs per camel-case word (so `termsAndConditions` → `TermAndCo A few published names aren't algorithmic, and the spec publishes some routes the SDK never shipped. Both live as data in `NamingOverrides.cs` — renames mirroring the SDK's hand-written AutoRest directives, and suppressions for routes that ship nothing — each entry citing its evidence: the directive when one exists, otherwise the shipped-command inventory. Examples: the `GET /users/{id}/calendar` rename to `…UserDefaultCalendar` (Calendar.md), the `Solution` prefix strip under `/solutions/*` with the BackupRestore exception (Bookings.md), and the self-referential `sites/{id}/sites` rename to `SubSite`/`GroupSubSite` (Sites.md) — without which the sub-sites cmdlets would collide with `Get-MgSite` itself. The generator fails loudly on any such file collision rather than silently overwriting. +On top of the curated entries sits a **derived** layer: `tools/Derive-CollisionResolutions.ps1` replays every route from the checked-in collision inventory (`data/collision-inventory.v1.0.txt`, captured with `--no-collision-data`) against the shipped-command inventory and emits `data/collision-suppressions.v1.0.json` and `data/collision-renames.v1.0.json` — one exact-match entry per contested method+route, each carrying its oracle evidence. The files are embedded into the generator at build time (generation never reads the 22 MB oracle), applied only when `GeneratorConfig.UseCollisionData` is set, and the script's `-Validate` mode fails if the checked-in files drift from a fresh derivation. Two routes in all of v1.0 are **deferred cross-path merges** — the published SDK serves `Get-MgGroupPhoto` from both `/photo` and `/photos`, and `Get-MgShareListItem` from both `/listItem` and `/list/items`, as parameter-set variants of one cmdlet; the generator keeps the singleton side and suppresses the collection side until cross-path parameter sets land (see [edge-cases/crosspath-merge-edge-cases.md](edge-cases/crosspath-merge-edge-cases.md)). With the derived data applied, a full v1.0 generation across all 39 modules produces zero collisions. + ## The one subtle part: list + item GET become one cmdlet Graph has two GETs for a resource — the collection (`GET …/messages`) and a single item (`GET …/messages/{message-id}`) — but the published SDK exposes **one** cmdlet, `Get-MgUserMessage`, that does both: no `-MessageId` lists them, a `-MessageId` fetches one. @@ -150,14 +152,14 @@ dotnet run --project tools/WrapperGenerator -- ` --include-path '/users/{user-id}/messages/{message-id}*#GET,DELETE' ``` -`-d` is the spec, `-o` the output folder, `-n` the namespace of the step-1 client the wrappers call. Each `--include-path` is a glob with an optional `#METHOD,METHOD` filter; omit them to generate every operation in the document. Output: `Shared.g.cs`, one `*.g.cs` per cmdlet (in namespace `MgPoC`), and a small `kiota-lock.json` noting the source spec. +`-d` is the spec, `-o` the output folder, `-n` the namespace of the step-1 client the wrappers call. Each `--include-path` is a glob with an optional `#METHOD,METHOD` filter; omit them to generate every operation in the document. Output: `Shared.g.cs`, one `*.g.cs` per cmdlet (in a namespace derived from `-n` by dropping its trailing `.Client`, e.g. `-n Microsoft.Graph.PowerShell.Mail.Client` emits into `Microsoft.Graph.PowerShell.Mail`), and a small `kiota-lock.json` noting the source spec. **Test** — two layers: ```powershell # 1. Naming rules pinned to published Microsoft.Graph names dotnet test tools/WrapperGenerator.Tests -# => Passed! - Failed: 0, Passed: 115, Total: 115 +# => Passed! - Failed: 0, Passed: 120, Total: 120 # 2. Parity gate: generate, then check every cmdlet name against Graph's own command inventory .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath @@ -168,7 +170,7 @@ The unit tests guard the naming rules (their expected values are real published ## Gaps / not done yet -- **Output isn't committed into `src/{Module}/`.** `tools/Build-WrapperModule.ps1` now turns a module into an importable build under `artifacts/` (kiota client + wrappers + csproj + PSD1; `tools/Test-WrapperModule.ps1` smoke-tests it), with generation reading the Kiota-compatible docs (`openApiDocs_KiotaCompat`) by default. The target design — wrappers committed into `src/{Module}/` with a per-module namespace instead of `MgPoC` — is still open. +- **Output isn't committed into `src/{Module}/`.** `tools/Build-WrapperModule.ps1` now turns a module into an importable build under `artifacts/` (kiota client + wrappers + csproj + PSD1; `tools/Test-WrapperModule.ps1` smoke-tests it), with generation reading the Kiota-compatible docs (`openApiDocs_KiotaCompat`) by default. Cmdlets now emit into a per-module namespace (not `MgPoC`) and the generated csproj references Authentication by a relative path, so both are ready to move; the exact target folder under `src/{Module}/{v1.0|beta}/` is still open — the existing AutoRest modules' `.gitignore` there excludes a folder literally named `generated`, so the wrapper output needs a different folder name or that pattern needs updating, or a commit there would silently produce an empty diff. - **No runtime base classes or real auth flow.** Shared paging, a proper `Connect-MgGraph`/session integration, and base cmdlet classes are a later phase. - **Body binding is shallow** — top-level primitive properties only; no nested/complex types beyond the `passwordProfile` special case. - **Some operation shapes aren't generated** — `$count`/`$ref`/`$value`, delta, OData actions/functions, and cast endpoints. diff --git a/tools/WrapperGenerator/WrapperGenerator.csproj b/tools/WrapperGenerator/WrapperGenerator.csproj index ee22043de8a..b6c3d744bd3 100644 --- a/tools/WrapperGenerator/WrapperGenerator.csproj +++ b/tools/WrapperGenerator/WrapperGenerator.csproj @@ -20,4 +20,10 @@ + + + + + diff --git a/tools/WrapperGenerator/data/collision-inventory.v1.0.txt b/tools/WrapperGenerator/data/collision-inventory.v1.0.txt new file mode 100644 index 00000000000..a39893d2c00 --- /dev/null +++ b/tools/WrapperGenerator/data/collision-inventory.v1.0.txt @@ -0,0 +1,212 @@ +Calendar :: GetMgGroupCalendarView.g.cs: 'Get-MgGroupCalendarView [Groups[GroupId].CalendarView]' collides with already-written 'Get-MgGroupCalendarView [Groups[GroupId].Calendar.CalendarView]' +Calendar :: GetMgUserCalendarView.g.cs: 'Get-MgUserCalendarView [Users[UserId].Calendars[CalendarId].CalendarView]' collides with already-written 'Get-MgUserCalendarView [Users[UserId].Calendar.CalendarView]' +Calendar :: GetMgUserCalendarView.g.cs: 'Get-MgUserCalendarView [Users[UserId].CalendarView]' collides with already-written 'Get-MgUserCalendarView [Users[UserId].Calendar.CalendarView]' +Files :: GetMgShareListItem.g.cs: 'Get-MgShareListItem [Shares[SharedDriveItemId].ListItem]' collides with already-written 'Get-MgShareListItem [Shares[SharedDriveItemId].List.Items]' +Groups :: NewMgGroupLifecyclePolicy.g.cs: 'New-MgGroupLifecyclePolicy [Groups[GroupId].GroupLifecyclePolicies]' collides with already-written 'New-MgGroupLifecyclePolicy [GroupLifecyclePolicies]' +Groups :: NewMgGroupSetting.g.cs: 'New-MgGroupSetting [GroupSettings]' collides with already-written 'New-MgGroupSetting [Groups[GroupId].Settings]' +Groups :: UpdateMgGroupSetting.g.cs: 'Update-MgGroupSetting [GroupSettings[GroupSettingId]]' collides with already-written 'Update-MgGroupSetting [Groups[GroupId].Settings[GroupSettingId]]' +Groups :: RemoveMgGroupSetting.g.cs: 'Remove-MgGroupSetting [GroupSettings[GroupSettingId]]' collides with already-written 'Remove-MgGroupSetting [Groups[GroupId].Settings[GroupSettingId]]' +Groups :: GetMgGroupPhoto.g.cs: 'Get-MgGroupPhoto [Groups[GroupId].Photos]' collides with already-written 'Get-MgGroupPhoto [Groups[GroupId].Photo]' +Groups :: GetMgGroupSetting.g.cs: 'Get-MgGroupSetting [Groups[GroupId].Settings[GroupSettingId]]' collides with already-written 'Get-MgGroupSetting [Groups[GroupId].Settings]' +Groups :: GetMgGroupSetting.g.cs: 'Get-MgGroupSetting [GroupSettings]' collides with already-written 'Get-MgGroupSetting [Groups[GroupId].Settings]' +Groups :: GetMgGroupSetting.g.cs: 'Get-MgGroupSetting [GroupSettings[GroupSettingId]]' collides with already-written 'Get-MgGroupSetting [Groups[GroupId].Settings]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementCatalogResourceRole.g.cs: 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceRole.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceRole.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope.g.cs: 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementCatalogResourceScope.g.cs: 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceScope.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceScope.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole.g.cs: 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole.g.cs: 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope.g.cs: 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope.g.cs: 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: NewMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole.g.cs: 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles]' collides with already-written 'New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' +Identity.Governance :: UpdateMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource.g.cs: 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: RemoveMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource.g.cs: 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Environment]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource.Environment]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Environment]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.Catalogs[AccessPackageCatalogId].Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource.Environment]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Environment]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Roles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceRoles[AccessPackageResourceRoleId].Resource.Scopes[AccessPackageResourceScopeId].Resource.Environment]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Environment]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId]]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource]' +Identity.Governance :: GetMgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment.g.cs: 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.ResourceScopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource.Environment]' collides with already-written 'Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment [IdentityGovernance.EntitlementManagement.ResourceRequests[AccessPackageResourceRequestId].Catalog.Resources[AccessPackageResourceId].Scopes[AccessPackageResourceScopeId].Resource.Roles[AccessPackageResourceRoleId].Resource.Environment]' +Notes :: GetMgGroupOnenoteNotebookSectionGroup.g.cs: 'Get-MgGroupOnenoteNotebookSectionGroup [Groups[GroupId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgGroupOnenoteNotebookSectionGroup [Groups[GroupId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgGroupOnenoteNotebookSectionGroup.g.cs: 'Get-MgGroupOnenoteNotebookSectionGroup [Groups[GroupId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgGroupOnenoteNotebookSectionGroup [Groups[GroupId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgGroupOnenoteNotebookSectionGroup.g.cs: 'Get-MgGroupOnenoteNotebookSectionGroup [Groups[GroupId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgGroupOnenoteNotebookSectionGroup [Groups[GroupId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgGroupOnenoteSectionGroup.g.cs: 'Get-MgGroupOnenoteSectionGroup [Groups[GroupId].Onenote.SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgGroupOnenoteSectionGroup [Groups[GroupId].Onenote.SectionGroups]' +Notes :: GetMgGroupOnenoteSectionGroup.g.cs: 'Get-MgGroupOnenoteSectionGroup [Groups[GroupId].Onenote.SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgGroupOnenoteSectionGroup [Groups[GroupId].Onenote.SectionGroups]' +Notes :: GetMgGroupOnenoteSectionGroup.g.cs: 'Get-MgGroupOnenoteSectionGroup [Groups[GroupId].Onenote.SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgGroupOnenoteSectionGroup [Groups[GroupId].Onenote.SectionGroups]' +Notes :: GetMgSiteOnenoteNotebookSectionGroup.g.cs: 'Get-MgSiteOnenoteNotebookSectionGroup [Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgSiteOnenoteNotebookSectionGroup [Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgSiteOnenoteNotebookSectionGroup.g.cs: 'Get-MgSiteOnenoteNotebookSectionGroup [Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgSiteOnenoteNotebookSectionGroup [Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgSiteOnenoteNotebookSectionGroup.g.cs: 'Get-MgSiteOnenoteNotebookSectionGroup [Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgSiteOnenoteNotebookSectionGroup [Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgSiteOnenoteSectionGroup.g.cs: 'Get-MgSiteOnenoteSectionGroup [Sites[SiteId].Onenote.SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgSiteOnenoteSectionGroup [Sites[SiteId].Onenote.SectionGroups]' +Notes :: GetMgSiteOnenoteSectionGroup.g.cs: 'Get-MgSiteOnenoteSectionGroup [Sites[SiteId].Onenote.SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgSiteOnenoteSectionGroup [Sites[SiteId].Onenote.SectionGroups]' +Notes :: GetMgSiteOnenoteSectionGroup.g.cs: 'Get-MgSiteOnenoteSectionGroup [Sites[SiteId].Onenote.SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgSiteOnenoteSectionGroup [Sites[SiteId].Onenote.SectionGroups]' +Notes :: GetMgUserOnenoteNotebookSectionGroup.g.cs: 'Get-MgUserOnenoteNotebookSectionGroup [Users[UserId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgUserOnenoteNotebookSectionGroup [Users[UserId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgUserOnenoteNotebookSectionGroup.g.cs: 'Get-MgUserOnenoteNotebookSectionGroup [Users[UserId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgUserOnenoteNotebookSectionGroup [Users[UserId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgUserOnenoteNotebookSectionGroup.g.cs: 'Get-MgUserOnenoteNotebookSectionGroup [Users[UserId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgUserOnenoteNotebookSectionGroup [Users[UserId].Onenote.Notebooks[NotebookId].SectionGroups]' +Notes :: GetMgUserOnenoteSectionGroup.g.cs: 'Get-MgUserOnenoteSectionGroup [Users[UserId].Onenote.SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgUserOnenoteSectionGroup [Users[UserId].Onenote.SectionGroups]' +Notes :: GetMgUserOnenoteSectionGroup.g.cs: 'Get-MgUserOnenoteSectionGroup [Users[UserId].Onenote.SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgUserOnenoteSectionGroup [Users[UserId].Onenote.SectionGroups]' +Notes :: GetMgUserOnenoteSectionGroup.g.cs: 'Get-MgUserOnenoteSectionGroup [Users[UserId].Onenote.SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgUserOnenoteSectionGroup [Users[UserId].Onenote.SectionGroups]' +Sites :: NewMgGroupSiteTermStoreGroupSetChild.g.cs: 'New-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children]' collides with already-written 'New-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children]' +Sites :: UpdateMgGroupSiteTermStoreGroupSetChild.g.cs: 'Update-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Update-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId]]' +Sites :: RemoveMgGroupSiteTermStoreGroupSetChild.g.cs: 'Remove-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Remove-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId]]' +Sites :: NewMgGroupSiteTermStoreGroupSetChildRelation.g.cs: 'New-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations]' collides with already-written 'New-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: UpdateMgGroupSiteTermStoreGroupSetChildRelation.g.cs: 'Update-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Update-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: RemoveMgGroupSiteTermStoreGroupSetChildRelation.g.cs: 'Remove-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Remove-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: NewMgGroupSiteTermStoreSetChild.g.cs: 'New-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children]' collides with already-written 'New-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: UpdateMgGroupSiteTermStoreSetChild.g.cs: 'Update-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Update-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId]]' +Sites :: RemoveMgGroupSiteTermStoreSetChild.g.cs: 'Remove-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Remove-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId]]' +Sites :: NewMgGroupSiteTermStoreSetChildRelation.g.cs: 'New-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations]' collides with already-written 'New-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: UpdateMgGroupSiteTermStoreSetChildRelation.g.cs: 'Update-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Update-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: RemoveMgGroupSiteTermStoreSetChildRelation.g.cs: 'Remove-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Remove-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: NewMgGroupSiteTermStoreSetParentGroupSetChild.g.cs: 'New-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children]' collides with already-written 'New-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: UpdateMgGroupSiteTermStoreSetParentGroupSetChild.g.cs: 'Update-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1]]' collides with already-written 'Update-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId]]' +Sites :: RemoveMgGroupSiteTermStoreSetParentGroupSetChild.g.cs: 'Remove-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1]]' collides with already-written 'Remove-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId]]' +Sites :: NewMgGroupSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'New-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations]' collides with already-written 'New-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: UpdateMgGroupSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Update-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId]]' collides with already-written 'Update-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: RemoveMgGroupSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Remove-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId]]' collides with already-written 'Remove-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: NewMgSiteTermStoreGroupSetChild.g.cs: 'New-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children]' collides with already-written 'New-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children]' +Sites :: UpdateMgSiteTermStoreGroupSetChild.g.cs: 'Update-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Update-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId]]' +Sites :: RemoveMgSiteTermStoreGroupSetChild.g.cs: 'Remove-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Remove-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId]]' +Sites :: NewMgSiteTermStoreGroupSetChildRelation.g.cs: 'New-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations]' collides with already-written 'New-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: UpdateMgSiteTermStoreGroupSetChildRelation.g.cs: 'Update-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Update-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: RemoveMgSiteTermStoreGroupSetChildRelation.g.cs: 'Remove-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Remove-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: NewMgSiteTermStoreSetChild.g.cs: 'New-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children]' collides with already-written 'New-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: UpdateMgSiteTermStoreSetChild.g.cs: 'Update-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Update-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId]]' +Sites :: RemoveMgSiteTermStoreSetChild.g.cs: 'Remove-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Remove-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId]]' +Sites :: NewMgSiteTermStoreSetChildRelation.g.cs: 'New-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations]' collides with already-written 'New-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: UpdateMgSiteTermStoreSetChildRelation.g.cs: 'Update-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Update-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: RemoveMgSiteTermStoreSetChildRelation.g.cs: 'Remove-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Remove-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: NewMgSiteTermStoreSetParentGroupSetChild.g.cs: 'New-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children]' collides with already-written 'New-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: UpdateMgSiteTermStoreSetParentGroupSetChild.g.cs: 'Update-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1]]' collides with already-written 'Update-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId]]' +Sites :: RemoveMgSiteTermStoreSetParentGroupSetChild.g.cs: 'Remove-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1]]' collides with already-written 'Remove-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId]]' +Sites :: NewMgSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'New-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations]' collides with already-written 'New-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: UpdateMgSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Update-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId]]' collides with already-written 'Update-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: RemoveMgSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Remove-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId]]' collides with already-written 'Remove-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId]]' +Sites :: GetMgGroupSiteOnenoteNotebookSectionGroup.g.cs: 'Get-MgGroupSiteOnenoteNotebookSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgGroupSiteOnenoteNotebookSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups]' +Sites :: GetMgGroupSiteOnenoteNotebookSectionGroup.g.cs: 'Get-MgGroupSiteOnenoteNotebookSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgGroupSiteOnenoteNotebookSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups]' +Sites :: GetMgGroupSiteOnenoteNotebookSectionGroup.g.cs: 'Get-MgGroupSiteOnenoteNotebookSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgGroupSiteOnenoteNotebookSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.Notebooks[NotebookId].SectionGroups]' +Sites :: GetMgGroupSiteOnenoteSectionGroup.g.cs: 'Get-MgGroupSiteOnenoteSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.SectionGroups[SectionGroupId]]' collides with already-written 'Get-MgGroupSiteOnenoteSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.SectionGroups]' +Sites :: GetMgGroupSiteOnenoteSectionGroup.g.cs: 'Get-MgGroupSiteOnenoteSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.SectionGroups[SectionGroupId].SectionGroups]' collides with already-written 'Get-MgGroupSiteOnenoteSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.SectionGroups]' +Sites :: GetMgGroupSiteOnenoteSectionGroup.g.cs: 'Get-MgGroupSiteOnenoteSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.SectionGroups[SectionGroupId].SectionGroups[SectionGroupId1]]' collides with already-written 'Get-MgGroupSiteOnenoteSectionGroup [Groups[GroupId].Sites[SiteId].Onenote.SectionGroups]' +Sites :: GetMgGroupSiteTermStoreGroupSetChild.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId]]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children]' +Sites :: GetMgGroupSiteTermStoreGroupSetChild.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children]' +Sites :: GetMgGroupSiteTermStoreGroupSetChild.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children]' +Sites :: GetMgGroupSiteTermStoreGroupSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreGroupSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreGroupSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreGroupSetChildRelationFromTerm.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChildRelationFromTerm [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations[RelationId].FromTerm]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChildRelationFromTerm [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].FromTerm]' +Sites :: GetMgGroupSiteTermStoreGroupSetChildRelationSet.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChildRelationSet [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations[RelationId].Set]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChildRelationSet [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].Set]' +Sites :: GetMgGroupSiteTermStoreGroupSetChildRelationToTerm.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChildRelationToTerm [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Relations[RelationId].ToTerm]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChildRelationToTerm [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].ToTerm]' +Sites :: GetMgGroupSiteTermStoreGroupSetChildSet.g.cs: 'Get-MgGroupSiteTermStoreGroupSetChildSet [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Set]' collides with already-written 'Get-MgGroupSiteTermStoreGroupSetChildSet [Groups[GroupId].Sites[SiteId].TermStore.Groups[GroupId1].Sets[SetId].Children[TermId].Children[TermId1].Set]' +Sites :: GetMgGroupSiteTermStoreSetChild.g.cs: 'Get-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId]]' collides with already-written 'Get-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: GetMgGroupSiteTermStoreSetChild.g.cs: 'Get-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children]' collides with already-written 'Get-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: GetMgGroupSiteTermStoreSetChild.g.cs: 'Get-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Get-MgGroupSiteTermStoreSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: GetMgGroupSiteTermStoreSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' collides with already-written 'Get-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations]' collides with already-written 'Get-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Get-MgGroupSiteTermStoreSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreSetChildRelationFromTerm.g.cs: 'Get-MgGroupSiteTermStoreSetChildRelationFromTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId].FromTerm]' collides with already-written 'Get-MgGroupSiteTermStoreSetChildRelationFromTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].FromTerm]' +Sites :: GetMgGroupSiteTermStoreSetChildRelationSet.g.cs: 'Get-MgGroupSiteTermStoreSetChildRelationSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId].Set]' collides with already-written 'Get-MgGroupSiteTermStoreSetChildRelationSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].Set]' +Sites :: GetMgGroupSiteTermStoreSetChildRelationToTerm.g.cs: 'Get-MgGroupSiteTermStoreSetChildRelationToTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId].ToTerm]' collides with already-written 'Get-MgGroupSiteTermStoreSetChildRelationToTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].ToTerm]' +Sites :: GetMgGroupSiteTermStoreSetChildSet.g.cs: 'Get-MgGroupSiteTermStoreSetChildSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Set]' collides with already-written 'Get-MgGroupSiteTermStoreSetChildSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Set]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChild.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId]]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChild.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChild.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1]]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChild [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId]]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId]]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId].FromTerm]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId].FromTerm]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChildRelationSet.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId].Set]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId].Set]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId].ToTerm]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId].ToTerm]' +Sites :: GetMgGroupSiteTermStoreSetParentGroupSetChildSet.g.cs: 'Get-MgGroupSiteTermStoreSetParentGroupSetChildSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Set]' collides with already-written 'Get-MgGroupSiteTermStoreSetParentGroupSetChildSet [Groups[GroupId].Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Set]' +Sites :: GetMgSiteTermStoreGroupSetChild.g.cs: 'Get-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId]]' collides with already-written 'Get-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children]' +Sites :: GetMgSiteTermStoreGroupSetChild.g.cs: 'Get-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children]' collides with already-written 'Get-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children]' +Sites :: GetMgSiteTermStoreGroupSetChild.g.cs: 'Get-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Get-MgSiteTermStoreGroupSetChild [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children]' +Sites :: GetMgSiteTermStoreGroupSetChildRelation.g.cs: 'Get-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' collides with already-written 'Get-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreGroupSetChildRelation.g.cs: 'Get-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations]' collides with already-written 'Get-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreGroupSetChildRelation.g.cs: 'Get-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Get-MgSiteTermStoreGroupSetChildRelation [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreGroupSetChildRelationFromTerm.g.cs: 'Get-MgSiteTermStoreGroupSetChildRelationFromTerm [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations[RelationId].FromTerm]' collides with already-written 'Get-MgSiteTermStoreGroupSetChildRelationFromTerm [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].FromTerm]' +Sites :: GetMgSiteTermStoreGroupSetChildRelationSet.g.cs: 'Get-MgSiteTermStoreGroupSetChildRelationSet [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations[RelationId].Set]' collides with already-written 'Get-MgSiteTermStoreGroupSetChildRelationSet [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].Set]' +Sites :: GetMgSiteTermStoreGroupSetChildRelationToTerm.g.cs: 'Get-MgSiteTermStoreGroupSetChildRelationToTerm [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Relations[RelationId].ToTerm]' collides with already-written 'Get-MgSiteTermStoreGroupSetChildRelationToTerm [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].ToTerm]' +Sites :: GetMgSiteTermStoreGroupSetChildSet.g.cs: 'Get-MgSiteTermStoreGroupSetChildSet [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Set]' collides with already-written 'Get-MgSiteTermStoreGroupSetChildSet [Sites[SiteId].TermStore.Groups[GroupId].Sets[SetId].Children[TermId].Children[TermId1].Set]' +Sites :: GetMgSiteTermStoreSetChild.g.cs: 'Get-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId]]' collides with already-written 'Get-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: GetMgSiteTermStoreSetChild.g.cs: 'Get-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children]' collides with already-written 'Get-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: GetMgSiteTermStoreSetChild.g.cs: 'Get-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1]]' collides with already-written 'Get-MgSiteTermStoreSetChild [Sites[SiteId].TermStore.Sets[SetId].Children]' +Sites :: GetMgSiteTermStoreSetChildRelation.g.cs: 'Get-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId]]' collides with already-written 'Get-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreSetChildRelation.g.cs: 'Get-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations]' collides with already-written 'Get-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreSetChildRelation.g.cs: 'Get-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId]]' collides with already-written 'Get-MgSiteTermStoreSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreSetChildRelationFromTerm.g.cs: 'Get-MgSiteTermStoreSetChildRelationFromTerm [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId].FromTerm]' collides with already-written 'Get-MgSiteTermStoreSetChildRelationFromTerm [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].FromTerm]' +Sites :: GetMgSiteTermStoreSetChildRelationSet.g.cs: 'Get-MgSiteTermStoreSetChildRelationSet [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId].Set]' collides with already-written 'Get-MgSiteTermStoreSetChildRelationSet [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].Set]' +Sites :: GetMgSiteTermStoreSetChildRelationToTerm.g.cs: 'Get-MgSiteTermStoreSetChildRelationToTerm [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Relations[RelationId].ToTerm]' collides with already-written 'Get-MgSiteTermStoreSetChildRelationToTerm [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Relations[RelationId].ToTerm]' +Sites :: GetMgSiteTermStoreSetChildSet.g.cs: 'Get-MgSiteTermStoreSetChildSet [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Set]' collides with already-written 'Get-MgSiteTermStoreSetChildSet [Sites[SiteId].TermStore.Sets[SetId].Children[TermId].Children[TermId1].Set]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChild.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId]]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChild.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChild.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1]]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChild [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId]]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChildRelation.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId]]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChildRelation [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChildRelationFromTerm.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChildRelationFromTerm [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId].FromTerm]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChildRelationFromTerm [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId].FromTerm]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChildRelationSet.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChildRelationSet [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId].Set]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChildRelationSet [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId].Set]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChildRelationToTerm.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChildRelationToTerm [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Relations[RelationId].ToTerm]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChildRelationToTerm [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Relations[RelationId].ToTerm]' +Sites :: GetMgSiteTermStoreSetParentGroupSetChildSet.g.cs: 'Get-MgSiteTermStoreSetParentGroupSetChildSet [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Set]' collides with already-written 'Get-MgSiteTermStoreSetParentGroupSetChildSet [Sites[SiteId].TermStore.Sets[SetId].ParentGroup.Sets[SetId1].Children[TermId].Children[TermId1].Set]' diff --git a/tools/WrapperGenerator/data/collision-renames.v1.0.json b/tools/WrapperGenerator/data/collision-renames.v1.0.json new file mode 100644 index 00000000000..5a42578886d --- /dev/null +++ b/tools/WrapperGenerator/data/collision-renames.v1.0.json @@ -0,0 +1,1254 @@ +[ + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRoleResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScopeResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}", + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles", + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}", + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes", + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScope" + ] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScope" + ] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}", + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles", + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource/environment", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource/environment" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}", + "action": "rename", + "evidence": { + "shipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementCatalogResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes", + "action": "rename", + "evidence": { + "shipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceScope" + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes" + ], + "counterpartShipsAs": [] + }, + "replacementNoun": "EntitlementManagementResourceRequestCatalogResourceScope" + } +] diff --git a/tools/WrapperGenerator/data/collision-resolution-ledger.v1.0.csv b/tools/WrapperGenerator/data/collision-resolution-ledger.v1.0.csv new file mode 100644 index 00000000000..4713ff7a9b7 --- /dev/null +++ b/tools/WrapperGenerator/data/collision-resolution-ledger.v1.0.csv @@ -0,0 +1,366 @@ +"Method","Uri","Modules","OurName","Action","ShipsAs","CounterpartUris","CounterpartShipsAs" +"DELETE","/groups/{}/settings/{}","Groups","Remove-MgGroupSetting","keep","Remove-MgGroupSetting","/groupsettings/{}","" +"DELETE","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}","Sites","Remove-MgGroupSiteTermStoreGroupSetChild","keep","Remove-MgGroupSiteTermStoreGroupSetChild","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","" +"DELETE","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Sites","Remove-MgGroupSiteTermStoreGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}","Remove-MgGroupSiteTermStoreGroupSetChild" +"DELETE","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Sites","Remove-MgGroupSiteTermStoreGroupSetChildRelation","keep","Remove-MgGroupSiteTermStoreGroupSetChildRelation","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","" +"DELETE","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Sites","Remove-MgGroupSiteTermStoreGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Remove-MgGroupSiteTermStoreGroupSetChildRelation" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/children/{}","Sites","Remove-MgGroupSiteTermStoreSetChild","keep","Remove-MgGroupSiteTermStoreSetChild","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}","" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}","Sites","Remove-MgGroupSiteTermStoreSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}","Remove-MgGroupSiteTermStoreSetChild" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Sites","Remove-MgGroupSiteTermStoreSetChildRelation","keep","Remove-MgGroupSiteTermStoreSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}","" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}","Sites","Remove-MgGroupSiteTermStoreSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Remove-MgGroupSiteTermStoreSetChildRelation" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Sites","Remove-MgGroupSiteTermStoreSetParentGroupSetChild","keep","Remove-MgGroupSiteTermStoreSetParentGroupSetChild","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Sites","Remove-MgGroupSiteTermStoreSetParentGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Remove-MgGroupSiteTermStoreSetParentGroupSetChild" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Sites","Remove-MgGroupSiteTermStoreSetParentGroupSetChildRelation","keep","Remove-MgGroupSiteTermStoreSetParentGroupSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","" +"DELETE","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Sites","Remove-MgGroupSiteTermStoreSetParentGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Remove-MgGroupSiteTermStoreSetParentGroupSetChildRelation" +"DELETE","/groupsettings/{}","Groups","Remove-MgGroupSetting","suppress","","/groups/{}/settings/{}","Remove-MgGroupSetting" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","rename","Remove-MgEntitlementManagementCatalogResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource","rename","Remove-MgEntitlementManagementCatalogResourceRoleResource","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","rename","Remove-MgEntitlementManagementCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource","rename","Remove-MgEntitlementManagementCatalogResourceRoleResourceScopeResource","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}","Remove-MgEntitlementManagementCatalogResourceRole" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource","Remove-MgEntitlementManagementCatalogResourceRoleResource" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}","Remove-MgEntitlementManagementCatalogResourceRoleResourceScope" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource","Remove-MgEntitlementManagementCatalogResourceRoleResourceScopeResource" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}","Remove-MgEntitlementManagementCatalogResourceScope" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource","rename","Remove-MgEntitlementManagementCatalogResourceScopeResource","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","rename","Remove-MgEntitlementManagementCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource","rename","Remove-MgEntitlementManagementCatalogResourceScopeResourceRoleResource","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","rename","Remove-MgEntitlementManagementCatalogResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}","" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource","Remove-MgEntitlementManagementCatalogResourceScopeResource" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}","Remove-MgEntitlementManagementCatalogResourceScopeResourceRole" +"DELETE","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource","Remove-MgEntitlementManagementCatalogResourceScopeResourceRoleResource" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}","Remove-MgEntitlementManagementResourceRequestCatalogResourceRole" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource","Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResource" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}","Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource","Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}","Remove-MgEntitlementManagementResourceRequestCatalogResourceScope" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","rename","Remove-MgEntitlementManagementResourceRequestCatalogResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}","" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource","Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResource" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}","Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" +"DELETE","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource","Identity.Governance","Remove-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource","Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" +"DELETE","/sites/{}/termstore/groups/{}/sets/{}/children/{}","Sites","Remove-MgSiteTermStoreGroupSetChild","keep","Remove-MgSiteTermStoreGroupSetChild","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","" +"DELETE","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Sites","Remove-MgSiteTermStoreGroupSetChild","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}","Remove-MgSiteTermStoreGroupSetChild" +"DELETE","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Sites","Remove-MgSiteTermStoreGroupSetChildRelation","keep","Remove-MgSiteTermStoreGroupSetChildRelation","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","" +"DELETE","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Sites","Remove-MgSiteTermStoreGroupSetChildRelation","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Remove-MgSiteTermStoreGroupSetChildRelation" +"DELETE","/sites/{}/termstore/sets/{}/children/{}","Sites","Remove-MgSiteTermStoreSetChild","keep","Remove-MgSiteTermStoreSetChild","/sites/{}/termstore/sets/{}/children/{}/children/{}","" +"DELETE","/sites/{}/termstore/sets/{}/children/{}/children/{}","Sites","Remove-MgSiteTermStoreSetChild","suppress","","/sites/{}/termstore/sets/{}/children/{}","Remove-MgSiteTermStoreSetChild" +"DELETE","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Sites","Remove-MgSiteTermStoreSetChildRelation","keep","Remove-MgSiteTermStoreSetChildRelation","/sites/{}/termstore/sets/{}/children/{}/relations/{}","" +"DELETE","/sites/{}/termstore/sets/{}/children/{}/relations/{}","Sites","Remove-MgSiteTermStoreSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Remove-MgSiteTermStoreSetChildRelation" +"DELETE","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Sites","Remove-MgSiteTermStoreSetParentGroupSetChild","keep","Remove-MgSiteTermStoreSetParentGroupSetChild","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","" +"DELETE","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Sites","Remove-MgSiteTermStoreSetParentGroupSetChild","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Remove-MgSiteTermStoreSetParentGroupSetChild" +"DELETE","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Sites","Remove-MgSiteTermStoreSetParentGroupSetChildRelation","keep","Remove-MgSiteTermStoreSetParentGroupSetChildRelation","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","" +"DELETE","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Sites","Remove-MgSiteTermStoreSetParentGroupSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Remove-MgSiteTermStoreSetParentGroupSetChildRelation" +"GET","/groups/{}/calendar/calendarview","Calendar","Get-MgGroupCalendarView","keep","Get-MgGroupCalendarView","/groups/{}/calendarview","" +"GET","/groups/{}/calendarview","Calendar","Get-MgGroupCalendarView","suppress","","/groups/{}/calendar/calendarview","Get-MgGroupCalendarView" +"GET","/groups/{}/onenote/notebooks/{}/sectiongroups","Notes","Get-MgGroupOnenoteNotebookSectionGroup","keep","Get-MgGroupOnenoteNotebookSectionGroup","/groups/{}/onenote/notebooks/{}/sectiongroups/{};/groups/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups;/groups/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Get-MgGroupOnenoteNotebookSectionGroup" +"GET","/groups/{}/onenote/notebooks/{}/sectiongroups/{}","Notes","Get-MgGroupOnenoteNotebookSectionGroup","keep","Get-MgGroupOnenoteNotebookSectionGroup","/groups/{}/onenote/notebooks/{}/sectiongroups","Get-MgGroupOnenoteNotebookSectionGroup" +"GET","/groups/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups","Notes","Get-MgGroupOnenoteNotebookSectionGroup","suppress","","/groups/{}/onenote/notebooks/{}/sectiongroups","Get-MgGroupOnenoteNotebookSectionGroup" +"GET","/groups/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Notes","Get-MgGroupOnenoteNotebookSectionGroup","suppress","","/groups/{}/onenote/notebooks/{}/sectiongroups","Get-MgGroupOnenoteNotebookSectionGroup" +"GET","/groups/{}/onenote/sectiongroups","Notes","Get-MgGroupOnenoteSectionGroup","keep","Get-MgGroupOnenoteSectionGroup","/groups/{}/onenote/sectiongroups/{};/groups/{}/onenote/sectiongroups/{}/sectiongroups;/groups/{}/onenote/sectiongroups/{}/sectiongroups/{}","Get-MgGroupOnenoteSectionGroup" +"GET","/groups/{}/onenote/sectiongroups/{}","Notes","Get-MgGroupOnenoteSectionGroup","keep","Get-MgGroupOnenoteSectionGroup","/groups/{}/onenote/sectiongroups","Get-MgGroupOnenoteSectionGroup" +"GET","/groups/{}/onenote/sectiongroups/{}/sectiongroups","Notes","Get-MgGroupOnenoteSectionGroup","suppress","","/groups/{}/onenote/sectiongroups","Get-MgGroupOnenoteSectionGroup" +"GET","/groups/{}/onenote/sectiongroups/{}/sectiongroups/{}","Notes","Get-MgGroupOnenoteSectionGroup","suppress","","/groups/{}/onenote/sectiongroups","Get-MgGroupOnenoteSectionGroup" +"GET","/groups/{}/photo","Groups","Get-MgGroupPhoto","keep","Get-MgGroupPhoto","/groups/{}/photos","Get-MgGroupPhoto" +"GET","/groups/{}/photos","Groups","Get-MgGroupPhoto","suppress-deferred","Get-MgGroupPhoto","/groups/{}/photo","Get-MgGroupPhoto" +"GET","/groups/{}/settings","Groups","Get-MgGroupSetting","keep","Get-MgGroupSetting","/groups/{}/settings/{};/groupsettings;/groupsettings/{}","Get-MgGroupSetting" +"GET","/groups/{}/settings/{}","Groups","Get-MgGroupSetting","keep","Get-MgGroupSetting","/groups/{}/settings","Get-MgGroupSetting" +"GET","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups","Sites","Get-MgGroupSiteOnenoteNotebookSectionGroup","keep","Get-MgGroupSiteOnenoteNotebookSectionGroup","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{};/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups;/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Get-MgGroupSiteOnenoteNotebookSectionGroup" +"GET","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{}","Sites","Get-MgGroupSiteOnenoteNotebookSectionGroup","keep","Get-MgGroupSiteOnenoteNotebookSectionGroup","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups","Get-MgGroupSiteOnenoteNotebookSectionGroup" +"GET","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups","Sites","Get-MgGroupSiteOnenoteNotebookSectionGroup","suppress","","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups","Get-MgGroupSiteOnenoteNotebookSectionGroup" +"GET","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Sites","Get-MgGroupSiteOnenoteNotebookSectionGroup","suppress","","/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups","Get-MgGroupSiteOnenoteNotebookSectionGroup" +"GET","/groups/{}/sites/{}/onenote/sectiongroups","Sites","Get-MgGroupSiteOnenoteSectionGroup","keep","Get-MgGroupSiteOnenoteSectionGroup","/groups/{}/sites/{}/onenote/sectiongroups/{};/groups/{}/sites/{}/onenote/sectiongroups/{}/sectiongroups;/groups/{}/sites/{}/onenote/sectiongroups/{}/sectiongroups/{}","Get-MgGroupSiteOnenoteSectionGroup" +"GET","/groups/{}/sites/{}/onenote/sectiongroups/{}","Sites","Get-MgGroupSiteOnenoteSectionGroup","keep","Get-MgGroupSiteOnenoteSectionGroup","/groups/{}/sites/{}/onenote/sectiongroups","Get-MgGroupSiteOnenoteSectionGroup" +"GET","/groups/{}/sites/{}/onenote/sectiongroups/{}/sectiongroups","Sites","Get-MgGroupSiteOnenoteSectionGroup","suppress","","/groups/{}/sites/{}/onenote/sectiongroups","Get-MgGroupSiteOnenoteSectionGroup" +"GET","/groups/{}/sites/{}/onenote/sectiongroups/{}/sectiongroups/{}","Sites","Get-MgGroupSiteOnenoteSectionGroup","suppress","","/groups/{}/sites/{}/onenote/sectiongroups","Get-MgGroupSiteOnenoteSectionGroup" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children","Sites","Get-MgGroupSiteTermStoreGroupSetChild","keep","Get-MgGroupSiteTermStoreGroupSetChild","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{};/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children;/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Get-MgGroupSiteTermStoreGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}","Sites","Get-MgGroupSiteTermStoreGroupSetChild","keep","Get-MgGroupSiteTermStoreGroupSetChild","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children","Get-MgGroupSiteTermStoreGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children","Sites","Get-MgGroupSiteTermStoreGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children","Get-MgGroupSiteTermStoreGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Sites","Get-MgGroupSiteTermStoreGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children","Get-MgGroupSiteTermStoreGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelation","keep","Get-MgGroupSiteTermStoreGroupSetChildRelation","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{};/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations;/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Get-MgGroupSiteTermStoreGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelation","keep","Get-MgGroupSiteTermStoreGroupSetChildRelation","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/fromterm","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelationFromTerm","keep","Get-MgGroupSiteTermStoreGroupSetChildRelationFromTerm","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/fromterm","" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/set","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelationSet","keep","Get-MgGroupSiteTermStoreGroupSetChildRelationSet","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/set","" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/toterm","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelationToTerm","keep","Get-MgGroupSiteTermStoreGroupSetChildRelationToTerm","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/toterm","" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/set","Sites","Get-MgGroupSiteTermStoreGroupSetChildSet","keep","Get-MgGroupSiteTermStoreGroupSetChildSet","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/set","" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/fromterm","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelationFromTerm","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/fromterm","Get-MgGroupSiteTermStoreGroupSetChildRelationFromTerm" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/set","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelationSet","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/set","Get-MgGroupSiteTermStoreGroupSetChildRelationSet" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/toterm","Sites","Get-MgGroupSiteTermStoreGroupSetChildRelationToTerm","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/toterm","Get-MgGroupSiteTermStoreGroupSetChildRelationToTerm" +"GET","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/set","Sites","Get-MgGroupSiteTermStoreGroupSetChildSet","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/set","Get-MgGroupSiteTermStoreGroupSetChildSet" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children","Sites","Get-MgGroupSiteTermStoreSetChild","keep","Get-MgGroupSiteTermStoreSetChild","/groups/{}/sites/{}/termstore/sets/{}/children/{};/groups/{}/sites/{}/termstore/sets/{}/children/{}/children;/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}","Get-MgGroupSiteTermStoreSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}","Sites","Get-MgGroupSiteTermStoreSetChild","keep","Get-MgGroupSiteTermStoreSetChild","/groups/{}/sites/{}/termstore/sets/{}/children","Get-MgGroupSiteTermStoreSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children","Sites","Get-MgGroupSiteTermStoreSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children","Get-MgGroupSiteTermStoreSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}","Sites","Get-MgGroupSiteTermStoreSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children","Get-MgGroupSiteTermStoreSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Sites","Get-MgGroupSiteTermStoreSetChildRelation","keep","Get-MgGroupSiteTermStoreSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{};/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations;/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}","Get-MgGroupSiteTermStoreSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Sites","Get-MgGroupSiteTermStoreSetChildRelation","keep","Get-MgGroupSiteTermStoreSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/fromterm","Sites","Get-MgGroupSiteTermStoreSetChildRelationFromTerm","keep","Get-MgGroupSiteTermStoreSetChildRelationFromTerm","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/fromterm","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/set","Sites","Get-MgGroupSiteTermStoreSetChildRelationSet","keep","Get-MgGroupSiteTermStoreSetChildRelationSet","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/set","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/toterm","Sites","Get-MgGroupSiteTermStoreSetChildRelationToTerm","keep","Get-MgGroupSiteTermStoreSetChildRelationToTerm","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/toterm","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/set","Sites","Get-MgGroupSiteTermStoreSetChildSet","keep","Get-MgGroupSiteTermStoreSetChildSet","/groups/{}/sites/{}/termstore/sets/{}/children/{}/set","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations","Sites","Get-MgGroupSiteTermStoreSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}","Sites","Get-MgGroupSiteTermStoreSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/fromterm","Sites","Get-MgGroupSiteTermStoreSetChildRelationFromTerm","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/fromterm","Get-MgGroupSiteTermStoreSetChildRelationFromTerm" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/set","Sites","Get-MgGroupSiteTermStoreSetChildRelationSet","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/set","Get-MgGroupSiteTermStoreSetChildRelationSet" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/toterm","Sites","Get-MgGroupSiteTermStoreSetChildRelationToTerm","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/toterm","Get-MgGroupSiteTermStoreSetChildRelationToTerm" +"GET","/groups/{}/sites/{}/termstore/sets/{}/children/{}/set","Sites","Get-MgGroupSiteTermStoreSetChildSet","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/set","Get-MgGroupSiteTermStoreSetChildSet" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChild","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChild","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{};/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children;/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Get-MgGroupSiteTermStoreSetParentGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChild","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChild","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Get-MgGroupSiteTermStoreSetParentGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Get-MgGroupSiteTermStoreSetParentGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Get-MgGroupSiteTermStoreSetParentGroupSetChild" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{};/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations;/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/fromterm","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/fromterm","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/set","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationSet","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationSet","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/set","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/toterm","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/toterm","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/set","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildSet","keep","Get-MgGroupSiteTermStoreSetParentGroupSetChildSet","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/set","" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/fromterm","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/fromterm","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/set","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationSet","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/set","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationSet" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/toterm","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/toterm","Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm" +"GET","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/set","Sites","Get-MgGroupSiteTermStoreSetParentGroupSetChildSet","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/set","Get-MgGroupSiteTermStoreSetParentGroupSetChildSet" +"GET","/groupsettings","Groups","Get-MgGroupSetting","suppress","","/groups/{}/settings","Get-MgGroupSetting" +"GET","/groupsettings/{}","Groups","Get-MgGroupSetting","suppress","","/groups/{}/settings","Get-MgGroupSetting" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","rename","Get-MgEntitlementManagementCatalogResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{};/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles;/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}","Get-MgEntitlementManagementCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","rename","Get-MgEntitlementManagementCatalogResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles","Get-MgEntitlementManagementCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource","rename","Get-MgEntitlementManagementCatalogResourceRoleResource","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceEnvironment","rename","Get-MgEntitlementManagementCatalogResourceRoleResourceEnvironment","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","rename","Get-MgEntitlementManagementCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{};/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes;/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}","Get-MgEntitlementManagementCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","rename","Get-MgEntitlementManagementCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes","Get-MgEntitlementManagementCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource","rename","Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResource","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment","rename","Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles","Get-MgEntitlementManagementCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles","Get-MgEntitlementManagementCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource","Get-MgEntitlementManagementCatalogResourceRoleResource" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/environment","Get-MgEntitlementManagementCatalogResourceRoleResourceEnvironment" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes","Get-MgEntitlementManagementCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes","Get-MgEntitlementManagementCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource","Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResource" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource/environment","Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{};/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes;/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}","Get-MgEntitlementManagementCatalogResourceScope" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource","rename","Get-MgEntitlementManagementCatalogResourceScopeResource","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceEnvironment","rename","Get-MgEntitlementManagementCatalogResourceScopeResourceEnvironment","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","rename","Get-MgEntitlementManagementCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{};/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles;/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}","Get-MgEntitlementManagementCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","rename","Get-MgEntitlementManagementCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles","Get-MgEntitlementManagementCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource","rename","Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResource","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment","rename","Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","rename","Get-MgEntitlementManagementCatalogResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","rename","Get-MgEntitlementManagementCatalogResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes","" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource","Get-MgEntitlementManagementCatalogResourceScopeResource" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/environment","Get-MgEntitlementManagementCatalogResourceScopeResourceEnvironment" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles","Get-MgEntitlementManagementCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles","Get-MgEntitlementManagementCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource","Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResource" +"GET","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource/environment","Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{};/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles;/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}","Get-MgEntitlementManagementResourceRequestCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles","Get-MgEntitlementManagementResourceRequestCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{};/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes;/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles","Get-MgEntitlementManagementResourceRequestCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles","Get-MgEntitlementManagementResourceRequestCatalogResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResource" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/environment","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource/environment","Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{};/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes;/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}","Get-MgEntitlementManagementResourceRequestCatalogResourceScope" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{};/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles;/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource/environment","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","rename","Get-MgEntitlementManagementResourceRequestCatalogResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes","" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResource" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/environment","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" +"GET","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource/environment","Identity.Governance","Get-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource/environment","Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment" +"GET","/shares/{}/list/items","Files","Get-MgShareListItem","suppress-deferred","Get-MgShareListItem","/shares/{}/listitem","Get-MgShareListItem" +"GET","/shares/{}/listitem","Files","Get-MgShareListItem","keep","Get-MgShareListItem","/shares/{}/list/items","Get-MgShareListItem" +"GET","/sites/{}/onenote/notebooks/{}/sectiongroups","Notes","Get-MgSiteOnenoteNotebookSectionGroup","keep","Get-MgSiteOnenoteNotebookSectionGroup","/sites/{}/onenote/notebooks/{}/sectiongroups/{};/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups;/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Get-MgSiteOnenoteNotebookSectionGroup" +"GET","/sites/{}/onenote/notebooks/{}/sectiongroups/{}","Notes","Get-MgSiteOnenoteNotebookSectionGroup","keep","Get-MgSiteOnenoteNotebookSectionGroup","/sites/{}/onenote/notebooks/{}/sectiongroups","Get-MgSiteOnenoteNotebookSectionGroup" +"GET","/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups","Notes","Get-MgSiteOnenoteNotebookSectionGroup","suppress","","/sites/{}/onenote/notebooks/{}/sectiongroups","Get-MgSiteOnenoteNotebookSectionGroup" +"GET","/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Notes","Get-MgSiteOnenoteNotebookSectionGroup","suppress","","/sites/{}/onenote/notebooks/{}/sectiongroups","Get-MgSiteOnenoteNotebookSectionGroup" +"GET","/sites/{}/onenote/sectiongroups","Notes","Get-MgSiteOnenoteSectionGroup","keep","Get-MgSiteOnenoteSectionGroup","/sites/{}/onenote/sectiongroups/{};/sites/{}/onenote/sectiongroups/{}/sectiongroups;/sites/{}/onenote/sectiongroups/{}/sectiongroups/{}","Get-MgSiteOnenoteSectionGroup" +"GET","/sites/{}/onenote/sectiongroups/{}","Notes","Get-MgSiteOnenoteSectionGroup","keep","Get-MgSiteOnenoteSectionGroup","/sites/{}/onenote/sectiongroups","Get-MgSiteOnenoteSectionGroup" +"GET","/sites/{}/onenote/sectiongroups/{}/sectiongroups","Notes","Get-MgSiteOnenoteSectionGroup","suppress","","/sites/{}/onenote/sectiongroups","Get-MgSiteOnenoteSectionGroup" +"GET","/sites/{}/onenote/sectiongroups/{}/sectiongroups/{}","Notes","Get-MgSiteOnenoteSectionGroup","suppress","","/sites/{}/onenote/sectiongroups","Get-MgSiteOnenoteSectionGroup" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children","Sites","Get-MgSiteTermStoreGroupSetChild","keep","Get-MgSiteTermStoreGroupSetChild","/sites/{}/termstore/groups/{}/sets/{}/children/{};/sites/{}/termstore/groups/{}/sets/{}/children/{}/children;/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Get-MgSiteTermStoreGroupSetChild" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}","Sites","Get-MgSiteTermStoreGroupSetChild","keep","Get-MgSiteTermStoreGroupSetChild","/sites/{}/termstore/groups/{}/sets/{}/children","Get-MgSiteTermStoreGroupSetChild" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children","Sites","Get-MgSiteTermStoreGroupSetChild","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children","Get-MgSiteTermStoreGroupSetChild" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Sites","Get-MgSiteTermStoreGroupSetChild","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children","Get-MgSiteTermStoreGroupSetChild" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Sites","Get-MgSiteTermStoreGroupSetChildRelation","keep","Get-MgSiteTermStoreGroupSetChildRelation","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{};/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations;/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Get-MgSiteTermStoreGroupSetChildRelation" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Sites","Get-MgSiteTermStoreGroupSetChildRelation","keep","Get-MgSiteTermStoreGroupSetChildRelation","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreGroupSetChildRelation" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/fromterm","Sites","Get-MgSiteTermStoreGroupSetChildRelationFromTerm","keep","Get-MgSiteTermStoreGroupSetChildRelationFromTerm","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/fromterm","" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/set","Sites","Get-MgSiteTermStoreGroupSetChildRelationSet","keep","Get-MgSiteTermStoreGroupSetChildRelationSet","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/set","" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/toterm","Sites","Get-MgSiteTermStoreGroupSetChildRelationToTerm","keep","Get-MgSiteTermStoreGroupSetChildRelationToTerm","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/toterm","" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/set","Sites","Get-MgSiteTermStoreGroupSetChildSet","keep","Get-MgSiteTermStoreGroupSetChildSet","/sites/{}/termstore/groups/{}/sets/{}/children/{}/set","" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations","Sites","Get-MgSiteTermStoreGroupSetChildRelation","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreGroupSetChildRelation" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Sites","Get-MgSiteTermStoreGroupSetChildRelation","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreGroupSetChildRelation" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/fromterm","Sites","Get-MgSiteTermStoreGroupSetChildRelationFromTerm","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/fromterm","Get-MgSiteTermStoreGroupSetChildRelationFromTerm" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/set","Sites","Get-MgSiteTermStoreGroupSetChildRelationSet","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/set","Get-MgSiteTermStoreGroupSetChildRelationSet" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/toterm","Sites","Get-MgSiteTermStoreGroupSetChildRelationToTerm","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/toterm","Get-MgSiteTermStoreGroupSetChildRelationToTerm" +"GET","/sites/{}/termstore/groups/{}/sets/{}/children/{}/set","Sites","Get-MgSiteTermStoreGroupSetChildSet","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/set","Get-MgSiteTermStoreGroupSetChildSet" +"GET","/sites/{}/termstore/sets/{}/children","Sites","Get-MgSiteTermStoreSetChild","keep","Get-MgSiteTermStoreSetChild","/sites/{}/termstore/sets/{}/children/{};/sites/{}/termstore/sets/{}/children/{}/children;/sites/{}/termstore/sets/{}/children/{}/children/{}","Get-MgSiteTermStoreSetChild" +"GET","/sites/{}/termstore/sets/{}/children/{}","Sites","Get-MgSiteTermStoreSetChild","keep","Get-MgSiteTermStoreSetChild","/sites/{}/termstore/sets/{}/children","Get-MgSiteTermStoreSetChild" +"GET","/sites/{}/termstore/sets/{}/children/{}/children","Sites","Get-MgSiteTermStoreSetChild","suppress","","/sites/{}/termstore/sets/{}/children","Get-MgSiteTermStoreSetChild" +"GET","/sites/{}/termstore/sets/{}/children/{}/children/{}","Sites","Get-MgSiteTermStoreSetChild","suppress","","/sites/{}/termstore/sets/{}/children","Get-MgSiteTermStoreSetChild" +"GET","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Sites","Get-MgSiteTermStoreSetChildRelation","keep","Get-MgSiteTermStoreSetChildRelation","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{};/sites/{}/termstore/sets/{}/children/{}/relations;/sites/{}/termstore/sets/{}/children/{}/relations/{}","Get-MgSiteTermStoreSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Sites","Get-MgSiteTermStoreSetChildRelation","keep","Get-MgSiteTermStoreSetChildRelation","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/fromterm","Sites","Get-MgSiteTermStoreSetChildRelationFromTerm","keep","Get-MgSiteTermStoreSetChildRelationFromTerm","/sites/{}/termstore/sets/{}/children/{}/relations/{}/fromterm","" +"GET","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/set","Sites","Get-MgSiteTermStoreSetChildRelationSet","keep","Get-MgSiteTermStoreSetChildRelationSet","/sites/{}/termstore/sets/{}/children/{}/relations/{}/set","" +"GET","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/toterm","Sites","Get-MgSiteTermStoreSetChildRelationToTerm","keep","Get-MgSiteTermStoreSetChildRelationToTerm","/sites/{}/termstore/sets/{}/children/{}/relations/{}/toterm","" +"GET","/sites/{}/termstore/sets/{}/children/{}/children/{}/set","Sites","Get-MgSiteTermStoreSetChildSet","keep","Get-MgSiteTermStoreSetChildSet","/sites/{}/termstore/sets/{}/children/{}/set","" +"GET","/sites/{}/termstore/sets/{}/children/{}/relations","Sites","Get-MgSiteTermStoreSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/children/{}/relations/{}","Sites","Get-MgSiteTermStoreSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/children/{}/relations/{}/fromterm","Sites","Get-MgSiteTermStoreSetChildRelationFromTerm","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/fromterm","Get-MgSiteTermStoreSetChildRelationFromTerm" +"GET","/sites/{}/termstore/sets/{}/children/{}/relations/{}/set","Sites","Get-MgSiteTermStoreSetChildRelationSet","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/set","Get-MgSiteTermStoreSetChildRelationSet" +"GET","/sites/{}/termstore/sets/{}/children/{}/relations/{}/toterm","Sites","Get-MgSiteTermStoreSetChildRelationToTerm","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/toterm","Get-MgSiteTermStoreSetChildRelationToTerm" +"GET","/sites/{}/termstore/sets/{}/children/{}/set","Sites","Get-MgSiteTermStoreSetChildSet","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/set","Get-MgSiteTermStoreSetChildSet" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Sites","Get-MgSiteTermStoreSetParentGroupSetChild","keep","Get-MgSiteTermStoreSetParentGroupSetChild","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{};/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children;/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Get-MgSiteTermStoreSetParentGroupSetChild" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Sites","Get-MgSiteTermStoreSetParentGroupSetChild","keep","Get-MgSiteTermStoreSetParentGroupSetChild","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Get-MgSiteTermStoreSetParentGroupSetChild" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children","Sites","Get-MgSiteTermStoreSetParentGroupSetChild","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Get-MgSiteTermStoreSetParentGroupSetChild" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Sites","Get-MgSiteTermStoreSetParentGroupSetChild","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Get-MgSiteTermStoreSetParentGroupSetChild" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelation","keep","Get-MgSiteTermStoreSetParentGroupSetChildRelation","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{};/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations;/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Get-MgSiteTermStoreSetParentGroupSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelation","keep","Get-MgSiteTermStoreSetParentGroupSetChildRelation","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreSetParentGroupSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/fromterm","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelationFromTerm","keep","Get-MgSiteTermStoreSetParentGroupSetChildRelationFromTerm","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/fromterm","" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/set","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelationSet","keep","Get-MgSiteTermStoreSetParentGroupSetChildRelationSet","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/set","" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/toterm","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelationToTerm","keep","Get-MgSiteTermStoreSetParentGroupSetChildRelationToTerm","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/toterm","" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/set","Sites","Get-MgSiteTermStoreSetParentGroupSetChildSet","keep","Get-MgSiteTermStoreSetParentGroupSetChildSet","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/set","" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreSetParentGroupSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Get-MgSiteTermStoreSetParentGroupSetChildRelation" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/fromterm","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelationFromTerm","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/fromterm","Get-MgSiteTermStoreSetParentGroupSetChildRelationFromTerm" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/set","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelationSet","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/set","Get-MgSiteTermStoreSetParentGroupSetChildRelationSet" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/toterm","Sites","Get-MgSiteTermStoreSetParentGroupSetChildRelationToTerm","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/toterm","Get-MgSiteTermStoreSetParentGroupSetChildRelationToTerm" +"GET","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/set","Sites","Get-MgSiteTermStoreSetParentGroupSetChildSet","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/set","Get-MgSiteTermStoreSetParentGroupSetChildSet" +"GET","/users/{}/calendar/calendarview","Calendar","Get-MgUserCalendarView","keep","Get-MgUserCalendarView","/users/{}/calendars/{}/calendarview;/users/{}/calendarview","" +"GET","/users/{}/calendars/{}/calendarview","Calendar","Get-MgUserCalendarView","suppress","","/users/{}/calendar/calendarview","Get-MgUserCalendarView" +"GET","/users/{}/calendarview","Calendar","Get-MgUserCalendarView","suppress","","/users/{}/calendar/calendarview","Get-MgUserCalendarView" +"GET","/users/{}/onenote/notebooks/{}/sectiongroups","Notes","Get-MgUserOnenoteNotebookSectionGroup","keep","Get-MgUserOnenoteNotebookSectionGroup","/users/{}/onenote/notebooks/{}/sectiongroups/{};/users/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups;/users/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Get-MgUserOnenoteNotebookSectionGroup" +"GET","/users/{}/onenote/notebooks/{}/sectiongroups/{}","Notes","Get-MgUserOnenoteNotebookSectionGroup","keep","Get-MgUserOnenoteNotebookSectionGroup","/users/{}/onenote/notebooks/{}/sectiongroups","Get-MgUserOnenoteNotebookSectionGroup" +"GET","/users/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups","Notes","Get-MgUserOnenoteNotebookSectionGroup","suppress","","/users/{}/onenote/notebooks/{}/sectiongroups","Get-MgUserOnenoteNotebookSectionGroup" +"GET","/users/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}","Notes","Get-MgUserOnenoteNotebookSectionGroup","suppress","","/users/{}/onenote/notebooks/{}/sectiongroups","Get-MgUserOnenoteNotebookSectionGroup" +"GET","/users/{}/onenote/sectiongroups","Notes","Get-MgUserOnenoteSectionGroup","keep","Get-MgUserOnenoteSectionGroup","/users/{}/onenote/sectiongroups/{};/users/{}/onenote/sectiongroups/{}/sectiongroups;/users/{}/onenote/sectiongroups/{}/sectiongroups/{}","Get-MgUserOnenoteSectionGroup" +"GET","/users/{}/onenote/sectiongroups/{}","Notes","Get-MgUserOnenoteSectionGroup","keep","Get-MgUserOnenoteSectionGroup","/users/{}/onenote/sectiongroups","Get-MgUserOnenoteSectionGroup" +"GET","/users/{}/onenote/sectiongroups/{}/sectiongroups","Notes","Get-MgUserOnenoteSectionGroup","suppress","","/users/{}/onenote/sectiongroups","Get-MgUserOnenoteSectionGroup" +"GET","/users/{}/onenote/sectiongroups/{}/sectiongroups/{}","Notes","Get-MgUserOnenoteSectionGroup","suppress","","/users/{}/onenote/sectiongroups","Get-MgUserOnenoteSectionGroup" +"PATCH","/groups/{}/settings/{}","Groups","Update-MgGroupSetting","keep","Update-MgGroupSetting","/groupsettings/{}","" +"PATCH","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}","Sites","Update-MgGroupSiteTermStoreGroupSetChild","keep","Update-MgGroupSiteTermStoreGroupSetChild","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","" +"PATCH","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Sites","Update-MgGroupSiteTermStoreGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}","Update-MgGroupSiteTermStoreGroupSetChild" +"PATCH","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Sites","Update-MgGroupSiteTermStoreGroupSetChildRelation","keep","Update-MgGroupSiteTermStoreGroupSetChildRelation","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","" +"PATCH","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Sites","Update-MgGroupSiteTermStoreGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Update-MgGroupSiteTermStoreGroupSetChildRelation" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/children/{}","Sites","Update-MgGroupSiteTermStoreSetChild","keep","Update-MgGroupSiteTermStoreSetChild","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}","" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}","Sites","Update-MgGroupSiteTermStoreSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}","Update-MgGroupSiteTermStoreSetChild" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Sites","Update-MgGroupSiteTermStoreSetChildRelation","keep","Update-MgGroupSiteTermStoreSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}","" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}","Sites","Update-MgGroupSiteTermStoreSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Update-MgGroupSiteTermStoreSetChildRelation" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Sites","Update-MgGroupSiteTermStoreSetParentGroupSetChild","keep","Update-MgGroupSiteTermStoreSetParentGroupSetChild","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Sites","Update-MgGroupSiteTermStoreSetParentGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Update-MgGroupSiteTermStoreSetParentGroupSetChild" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Sites","Update-MgGroupSiteTermStoreSetParentGroupSetChildRelation","keep","Update-MgGroupSiteTermStoreSetParentGroupSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","" +"PATCH","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Sites","Update-MgGroupSiteTermStoreSetParentGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Update-MgGroupSiteTermStoreSetParentGroupSetChildRelation" +"PATCH","/groupsettings/{}","Groups","Update-MgGroupSetting","suppress","","/groups/{}/settings/{}","Update-MgGroupSetting" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","rename","Update-MgEntitlementManagementCatalogResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","rename","Update-MgEntitlementManagementCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}","Update-MgEntitlementManagementCatalogResourceRole" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}","Update-MgEntitlementManagementCatalogResourceRoleResourceScope" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}","Update-MgEntitlementManagementCatalogResourceScope" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","rename","Update-MgEntitlementManagementCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","rename","Update-MgEntitlementManagementCatalogResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}","Update-MgEntitlementManagementCatalogResourceScopeResourceRole" +"PATCH","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","rename","Update-MgEntitlementManagementResourceRequestCatalogResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","rename","Update-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}","Update-MgEntitlementManagementResourceRequestCatalogResourceRole" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}","Update-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}","Update-MgEntitlementManagementResourceRequestCatalogResourceScope" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","rename","Update-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","rename","Update-MgEntitlementManagementResourceRequestCatalogResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource","" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}","Update-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" +"PATCH","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource","Identity.Governance","Update-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource","" +"PATCH","/sites/{}/termstore/groups/{}/sets/{}/children/{}","Sites","Update-MgSiteTermStoreGroupSetChild","keep","Update-MgSiteTermStoreGroupSetChild","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","" +"PATCH","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}","Sites","Update-MgSiteTermStoreGroupSetChild","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}","Update-MgSiteTermStoreGroupSetChild" +"PATCH","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Sites","Update-MgSiteTermStoreGroupSetChildRelation","keep","Update-MgSiteTermStoreGroupSetChildRelation","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","" +"PATCH","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}","Sites","Update-MgSiteTermStoreGroupSetChildRelation","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}","Update-MgSiteTermStoreGroupSetChildRelation" +"PATCH","/sites/{}/termstore/sets/{}/children/{}","Sites","Update-MgSiteTermStoreSetChild","keep","Update-MgSiteTermStoreSetChild","/sites/{}/termstore/sets/{}/children/{}/children/{}","" +"PATCH","/sites/{}/termstore/sets/{}/children/{}/children/{}","Sites","Update-MgSiteTermStoreSetChild","suppress","","/sites/{}/termstore/sets/{}/children/{}","Update-MgSiteTermStoreSetChild" +"PATCH","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Sites","Update-MgSiteTermStoreSetChildRelation","keep","Update-MgSiteTermStoreSetChildRelation","/sites/{}/termstore/sets/{}/children/{}/relations/{}","" +"PATCH","/sites/{}/termstore/sets/{}/children/{}/relations/{}","Sites","Update-MgSiteTermStoreSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}","Update-MgSiteTermStoreSetChildRelation" +"PATCH","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Sites","Update-MgSiteTermStoreSetParentGroupSetChild","keep","Update-MgSiteTermStoreSetParentGroupSetChild","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","" +"PATCH","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}","Sites","Update-MgSiteTermStoreSetParentGroupSetChild","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}","Update-MgSiteTermStoreSetParentGroupSetChild" +"PATCH","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Sites","Update-MgSiteTermStoreSetParentGroupSetChildRelation","keep","Update-MgSiteTermStoreSetParentGroupSetChildRelation","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","" +"PATCH","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}","Sites","Update-MgSiteTermStoreSetParentGroupSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}","Update-MgSiteTermStoreSetParentGroupSetChildRelation" +"POST","/grouplifecyclepolicies","Groups","New-MgGroupLifecyclePolicy","keep","New-MgGroupLifecyclePolicy","/groups/{}/grouplifecyclepolicies","" +"POST","/groups/{}/grouplifecyclepolicies","Groups","New-MgGroupLifecyclePolicy","suppress","","/grouplifecyclepolicies","New-MgGroupLifecyclePolicy" +"POST","/groups/{}/settings","Groups","New-MgGroupSetting","keep","New-MgGroupSetting","/groupsettings","" +"POST","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children","Sites","New-MgGroupSiteTermStoreGroupSetChild","keep","New-MgGroupSiteTermStoreGroupSetChild","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children","" +"POST","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children","Sites","New-MgGroupSiteTermStoreGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children","New-MgGroupSiteTermStoreGroupSetChild" +"POST","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Sites","New-MgGroupSiteTermStoreGroupSetChildRelation","keep","New-MgGroupSiteTermStoreGroupSetChildRelation","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations","" +"POST","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations","Sites","New-MgGroupSiteTermStoreGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","New-MgGroupSiteTermStoreGroupSetChildRelation" +"POST","/groups/{}/sites/{}/termstore/sets/{}/children","Sites","New-MgGroupSiteTermStoreSetChild","keep","New-MgGroupSiteTermStoreSetChild","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children","" +"POST","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children","Sites","New-MgGroupSiteTermStoreSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children","New-MgGroupSiteTermStoreSetChild" +"POST","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Sites","New-MgGroupSiteTermStoreSetChildRelation","keep","New-MgGroupSiteTermStoreSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations","" +"POST","/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations","Sites","New-MgGroupSiteTermStoreSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","New-MgGroupSiteTermStoreSetChildRelation" +"POST","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Sites","New-MgGroupSiteTermStoreSetParentGroupSetChild","keep","New-MgGroupSiteTermStoreSetParentGroupSetChild","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children","" +"POST","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children","Sites","New-MgGroupSiteTermStoreSetParentGroupSetChild","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","New-MgGroupSiteTermStoreSetParentGroupSetChild" +"POST","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Sites","New-MgGroupSiteTermStoreSetParentGroupSetChildRelation","keep","New-MgGroupSiteTermStoreSetParentGroupSetChildRelation","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations","" +"POST","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations","Sites","New-MgGroupSiteTermStoreSetParentGroupSetChildRelation","suppress","","/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","New-MgGroupSiteTermStoreSetParentGroupSetChildRelation" +"POST","/groupsettings","Groups","New-MgGroupSetting","suppress","","/groups/{}/settings","New-MgGroupSetting" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","rename","New-MgEntitlementManagementCatalogResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles","" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","rename","New-MgEntitlementManagementCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes","" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles","New-MgEntitlementManagementCatalogResourceRole" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes","New-MgEntitlementManagementCatalogResourceRoleResourceScope" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes","New-MgEntitlementManagementCatalogResourceScope" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","rename","New-MgEntitlementManagementCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles","" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceScope","rename","New-MgEntitlementManagementCatalogResourceScope","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes","" +"POST","/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles","New-MgEntitlementManagementCatalogResourceScopeResourceRole" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","rename","New-MgEntitlementManagementResourceRequestCatalogResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles","" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","rename","New-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes","" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles","New-MgEntitlementManagementResourceRequestCatalogResourceRole" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceRoleResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes","New-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes","New-MgEntitlementManagementResourceRequestCatalogResourceScope" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","rename","New-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles","" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScope","rename","New-MgEntitlementManagementResourceRequestCatalogResourceScope","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes","" +"POST","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles","Identity.Governance","New-MgIdentityGovernanceEntitlementManagementResourceRequestCatalogResourceScopeResourceRole","suppress","","/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles","New-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" +"POST","/sites/{}/termstore/groups/{}/sets/{}/children","Sites","New-MgSiteTermStoreGroupSetChild","keep","New-MgSiteTermStoreGroupSetChild","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children","" +"POST","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children","Sites","New-MgSiteTermStoreGroupSetChild","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children","New-MgSiteTermStoreGroupSetChild" +"POST","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","Sites","New-MgSiteTermStoreGroupSetChildRelation","keep","New-MgSiteTermStoreGroupSetChildRelation","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations","" +"POST","/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations","Sites","New-MgSiteTermStoreGroupSetChildRelation","suppress","","/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations","New-MgSiteTermStoreGroupSetChildRelation" +"POST","/sites/{}/termstore/sets/{}/children","Sites","New-MgSiteTermStoreSetChild","keep","New-MgSiteTermStoreSetChild","/sites/{}/termstore/sets/{}/children/{}/children","" +"POST","/sites/{}/termstore/sets/{}/children/{}/children","Sites","New-MgSiteTermStoreSetChild","suppress","","/sites/{}/termstore/sets/{}/children","New-MgSiteTermStoreSetChild" +"POST","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","Sites","New-MgSiteTermStoreSetChildRelation","keep","New-MgSiteTermStoreSetChildRelation","/sites/{}/termstore/sets/{}/children/{}/relations","" +"POST","/sites/{}/termstore/sets/{}/children/{}/relations","Sites","New-MgSiteTermStoreSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/children/{}/children/{}/relations","New-MgSiteTermStoreSetChildRelation" +"POST","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","Sites","New-MgSiteTermStoreSetParentGroupSetChild","keep","New-MgSiteTermStoreSetParentGroupSetChild","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children","" +"POST","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children","Sites","New-MgSiteTermStoreSetParentGroupSetChild","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children","New-MgSiteTermStoreSetParentGroupSetChild" +"POST","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","Sites","New-MgSiteTermStoreSetParentGroupSetChildRelation","keep","New-MgSiteTermStoreSetParentGroupSetChildRelation","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations","" +"POST","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations","Sites","New-MgSiteTermStoreSetParentGroupSetChildRelation","suppress","","/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations","New-MgSiteTermStoreSetParentGroupSetChildRelation" diff --git a/tools/WrapperGenerator/data/collision-suppressions.v1.0.json b/tools/WrapperGenerator/data/collision-suppressions.v1.0.json new file mode 100644 index 00000000000..261407ed1be --- /dev/null +++ b/tools/WrapperGenerator/data/collision-suppressions.v1.0.json @@ -0,0 +1,3792 @@ +[ + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Remove-MgGroupSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Remove-MgGroupSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Remove-MgGroupSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Remove-MgGroupSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Remove-MgGroupSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Remove-MgGroupSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Groups" + ], + "method": "DELETE", + "uri": "/groupsettings/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/settings/{}" + ], + "counterpartShipsAs": [ + "Remove-MgGroupSetting" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceRoleResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementCatalogResourceScopeResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "DELETE", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [ + "Remove-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Remove-MgSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Remove-MgSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/sites/{}/termstore/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Remove-MgSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Remove-MgSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Remove-MgSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "DELETE", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Remove-MgSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Calendar" + ], + "method": "GET", + "uri": "/groups/{}/calendarview", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/calendar/calendarview" + ], + "counterpartShipsAs": [ + "Get-MgGroupCalendarView" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/groups/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/groups/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/groups/{}/onenote/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/groups/{}/onenote/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Groups" + ], + "method": "GET", + "uri": "/groups/{}/photos", + "action": "suppress", + "evidence": { + "shipsAs": [ + "Get-MgGroupPhoto" + ], + "counterpartUris": [ + "/groups/{}/photo" + ], + "counterpartShipsAs": [ + "Get-MgGroupPhoto" + ] + }, + "deferredCrossPathMerge": true + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/onenote/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/onenote/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/fromterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/fromterm" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChildRelationFromTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChildRelationSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/toterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/toterm" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChildRelationToTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreGroupSetChildSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/fromterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/fromterm" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChildRelationFromTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChildRelationSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}/toterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/toterm" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChildRelationToTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetChildSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/fromterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/fromterm" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationFromTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/toterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/toterm" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChildRelationToTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgGroupSiteTermStoreSetParentGroupSetChildSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Groups" + ], + "method": "GET", + "uri": "/groupsettings", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/settings" + ], + "counterpartShipsAs": [ + "Get-MgGroupSetting" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Groups" + ], + "method": "GET", + "uri": "/groupsettings/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/settings" + ], + "counterpartShipsAs": [ + "Get-MgGroupSetting" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceRoleResourceScopeResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}", + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes", + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementCatalogResourceScopeResourceRoleResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScopeResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes", + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResource" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "GET", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource/environment", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource/environment" + ], + "counterpartShipsAs": [ + "Get-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRoleResourceEnvironment" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Files" + ], + "method": "GET", + "uri": "/shares/{}/list/items", + "action": "suppress", + "evidence": { + "shipsAs": [ + "Get-MgShareListItem" + ], + "counterpartUris": [ + "/shares/{}/listitem" + ], + "counterpartShipsAs": [ + "Get-MgShareListItem" + ] + }, + "deferredCrossPathMerge": true + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgSiteOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/sites/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgSiteOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/sites/{}/onenote/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgSiteOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/sites/{}/onenote/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgSiteOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/fromterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/fromterm" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChildRelationFromTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChildRelationSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}/toterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}/toterm" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChildRelationToTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreGroupSetChildSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations/{}/fromterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/fromterm" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChildRelationFromTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChildRelationSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations/{}/toterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}/toterm" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChildRelationToTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/children/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetChildSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/fromterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/fromterm" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChildRelationFromTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChildRelationSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}/toterm", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}/toterm" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChildRelationToTerm" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "GET", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/set", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/set" + ], + "counterpartShipsAs": [ + "Get-MgSiteTermStoreSetParentGroupSetChildSet" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Calendar" + ], + "method": "GET", + "uri": "/users/{}/calendars/{}/calendarview", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/users/{}/calendar/calendarview" + ], + "counterpartShipsAs": [ + "Get-MgUserCalendarView" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Calendar" + ], + "method": "GET", + "uri": "/users/{}/calendarview", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/users/{}/calendar/calendarview" + ], + "counterpartShipsAs": [ + "Get-MgUserCalendarView" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/users/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/users/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgUserOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/users/{}/onenote/notebooks/{}/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/users/{}/onenote/notebooks/{}/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgUserOnenoteNotebookSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/users/{}/onenote/sectiongroups/{}/sectiongroups", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/users/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgUserOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Notes" + ], + "method": "GET", + "uri": "/users/{}/onenote/sectiongroups/{}/sectiongroups/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/users/{}/onenote/sectiongroups" + ], + "counterpartShipsAs": [ + "Get-MgUserOnenoteSectionGroup" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Update-MgGroupSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Update-MgGroupSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Update-MgGroupSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Update-MgGroupSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Update-MgGroupSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Update-MgGroupSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Groups" + ], + "method": "PATCH", + "uri": "/groupsettings/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/settings/{}" + ], + "counterpartShipsAs": [ + "Update-MgGroupSetting" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}" + ], + "counterpartShipsAs": [ + "Update-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "PATCH", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles/{}/resource", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles/{}/resource" + ], + "counterpartShipsAs": [] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Update-MgSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Update-MgSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/sites/{}/termstore/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Update-MgSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Update-MgSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}" + ], + "counterpartShipsAs": [ + "Update-MgSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "PATCH", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations/{}", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations/{}" + ], + "counterpartShipsAs": [ + "Update-MgSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Groups" + ], + "method": "POST", + "uri": "/groups/{}/grouplifecyclepolicies", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/grouplifecyclepolicies" + ], + "counterpartShipsAs": [ + "New-MgGroupLifecyclePolicy" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children" + ], + "counterpartShipsAs": [ + "New-MgGroupSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "New-MgGroupSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children" + ], + "counterpartShipsAs": [ + "New-MgGroupSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "New-MgGroupSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children" + ], + "counterpartShipsAs": [ + "New-MgGroupSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "New-MgGroupSiteTermStoreSetParentGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Groups" + ], + "method": "POST", + "uri": "/groupsettings", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/groups/{}/settings" + ], + "counterpartShipsAs": [ + "New-MgGroupSetting" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/roles/{}/resource/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/catalogs/{}/resourcescopes/{}/resource/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/catalogs/{}/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/roles/{}/resource/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourceroles/{}/resource/scopes" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceRoleResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceScope" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Identity.Governance" + ], + "method": "POST", + "uri": "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resourcescopes/{}/resource/roles", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/identitygovernance/entitlementmanagement/resourcerequests/{}/catalog/resources/{}/scopes/{}/resource/roles" + ], + "counterpartShipsAs": [ + "New-MgEntitlementManagementResourceRequestCatalogResourceScopeResourceRole" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children" + ], + "counterpartShipsAs": [ + "New-MgSiteTermStoreGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/sites/{}/termstore/groups/{}/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/groups/{}/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "New-MgSiteTermStoreGroupSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/sites/{}/termstore/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children" + ], + "counterpartShipsAs": [ + "New-MgSiteTermStoreSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/sites/{}/termstore/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "New-MgSiteTermStoreSetChildRelation" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children" + ], + "counterpartShipsAs": [ + "New-MgSiteTermStoreSetParentGroupSetChild" + ] + } + }, + { + "apiVersion": "v1.0", + "modules": [ + "Sites" + ], + "method": "POST", + "uri": "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/relations", + "action": "suppress", + "evidence": { + "shipsAs": [ + null + ], + "counterpartUris": [ + "/sites/{}/termstore/sets/{}/parentgroup/sets/{}/children/{}/children/{}/relations" + ], + "counterpartShipsAs": [ + "New-MgSiteTermStoreSetParentGroupSetChildRelation" + ] + } + } +] diff --git a/tools/WrapperGenerator/edge-cases/crosspath-merge-edge-cases.md b/tools/WrapperGenerator/edge-cases/crosspath-merge-edge-cases.md new file mode 100644 index 00000000000..6d13334f7a9 --- /dev/null +++ b/tools/WrapperGenerator/edge-cases/crosspath-merge-edge-cases.md @@ -0,0 +1,49 @@ +# Cross-path variant merge edge cases + +This file covers one class of issue: **cross-path variant merges** — cases where the +published SDK serves ONE cmdlet from several unrelated request URIs, as AutoRest parameter-set +variants. The wrapper generator emits one cmdlet per route (plus the list/item dispatcher), so +it cannot express these yet; the resolution policy is deterministic deferral. The derivation +sweep of every v1.0 collision route (`tools/Derive-CollisionResolutions.ps1`, ledger in +`artifacts/collision-resolution-ledger.v1.0.csv`) found exactly two. + +**Policy:** among same-command routes, the shallowest list/item pair survives (fewest path +parameters; tie broken by shortest, then ordinal — fully deterministic); the other routes are +suppressed with `deferredCrossPathMerge: true` in `data/collision-suppressions.v1.0.json`. +They come back when cross-path parameter sets are implemented (tracked with the operation +shapes / parameter-set work). + +## Group photo: `/photo` vs `/photos` + +- **Class:** crosspath-merge +- **Status:** workaround (singleton kept, collection deferred) +- **Evidence:** oracle ships `Get-MgGroupPhoto` for both `GET /groups/{id}/photo` and + `GET /groups/{id}/photos`; `/photos/{id}` ships nothing. Mirrors the `/users/{id}/photo(s)` + pair already curated in `NamingOverrides.cs`. +- **Decision:** generate from the `/photo` singleton (the primary published variant); defer + `/photos` (the all-sizes collection) until parameter sets can put both URIs behind one + cmdlet. +- **Migration impact:** `Get-MgGroupPhoto` exists with identical name; listing all photo + sizes via `-All`-style enumeration is not available until the deferral lifts. +- **References:** `data/collision-suppressions.v1.0.json` (`GET /groups/{}/photos`), + DerivedCollisionResolutionsTests. + +## Shared list items: `/listItem` vs `/list/items` + +- **Class:** crosspath-merge +- **Status:** workaround (singleton kept, collection deferred) +- **Evidence:** oracle ships `Get-MgShareListItem` for both `GET /shares/{id}/listItem` and + `GET /shares/{id}/list/items`; the bare `/list/items/{id}` item GET ships nothing (curated + suppression, `NamingOverrides.cs`). +- **Decision:** generate from the `/listItem` singleton; defer the `/list/items` collection. +- **Migration impact:** `Get-MgShareListItem` exists with identical name; enumerating a + shared list's items through this cmdlet is not available until the deferral lifts. +- **References:** `data/collision-suppressions.v1.0.json` (`GET /shares/{}/list/items`), + DerivedCollisionResolutionsTests. + +## Status summary + +| Case | Class | Status | +|---|---|---| +| `/groups/{id}/photo` vs `/photos` | crosspath-merge | workaround | +| `/shares/{id}/listItem` vs `/list/items` | crosspath-merge | workaround | From 5608540b483c468975548e83120748c53576af63 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Thu, 13 Aug 2026 21:20:54 -0700 Subject: [PATCH 09/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tools/Build-WrapperModule.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Build-WrapperModule.ps1 b/tools/Build-WrapperModule.ps1 index 7630a2563ef..52a42169f6c 100644 --- a/tools/Build-WrapperModule.ps1 +++ b/tools/Build-WrapperModule.ps1 @@ -148,7 +148,7 @@ function Build-OneModule { $result.FailedAt = 'wrapper-generator' $lines = @($wrapperOut | ForEach-Object { "$_" }) $exception = $lines | Where-Object { $_ -match 'Unhandled exception|Exception:' } | Select-Object -First 1 - $exceptionIndex = if ($exception) { $lines.IndexOf($exception) } else { -1 } + $exceptionIndex = if ($exception) { [Array]::IndexOf($lines, $exception) } else { -1 } $result.Error = if ($exceptionIndex -ge 0) { ($lines[$exceptionIndex..([Math]::Min($exceptionIndex + 5, $lines.Count - 1))] | Where-Object { $_ -notmatch '^\s+at ' }) -join ' | ' } else { From b7bf79a2b246a8007edbf6ae51fa587e42e33f38 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Thu, 13 Aug 2026 21:21:09 -0700 Subject: [PATCH 10/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../WrapperGenerator/PowerShellWrapperGenerationService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs index 82974e0db3e..5c1b6a8e1a6 100644 --- a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs +++ b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs @@ -315,7 +315,12 @@ private async Task EmitGetOperationsAsync(List getOpera private async Task WriteCmdletFileAsync(CmdletNaming naming, string source, CancellationToken cancellationToken) { - var fileName = naming.ClassName.Replace("Command", "", StringComparison.Ordinal) + ".g.cs"; + const string cmdletClassSuffix = "Command"; + var className = naming.ClassName; + var fileBaseName = className.EndsWith(cmdletClassSuffix, StringComparison.Ordinal) + ? className[..^cmdletClassSuffix.Length] + : className; + var fileName = fileBaseName + ".g.cs"; // Both colliding cmdlets usually share the same name, so the builder expression (the // request path) is what actually identifies which two operations collided. var cmdletName = $"{naming.VerbName}-{naming.Noun} [{naming.BuilderExpression}]"; From e287a9196cae942b1837a134d3f93a73703beeb0 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Thu, 13 Aug 2026 22:20:30 -0700 Subject: [PATCH 11/13] feat(wrapper-generator): complete v1.0 request-body binding Request bodies bound only top-level primitives, so 4,466 property occurrences across the v1.0 specs had no parameter. Every shape the classifier reaches now binds: referenced models and enums, formatted strings, schema-less UntypedNode values (converted on assignment, nulls dropped to match the published SDK's AddIf), and the numeric INF/NaN union. The invented -Password pair is replaced by the published -PasswordProfile. New gates verify it - omission oracle, coverage sweep, inventory diff, runtime conversions: 0 unbound across all 38 specs, 35 modules build and import, 148 tests. The pre-existing naming-parity gap is tracked separately. --- .gitignore | 4 + tools/Build-WrapperModule.ps1 | 32 +- tools/Compare-WrapperCmdletNames.ps1 | 6 +- tools/Compare-WrapperOperationInventory.ps1 | 105 ++++ tools/Measure-BodyPropertyCoverage.ps1 | 106 ++++ tools/New-WrapperOutputManifest.ps1 | 98 ++++ tools/Test-BodyBindingCoverage.ps1 | 296 ++++++++++ tools/Test-WrapperModule.ps1 | 225 +++++++- tools/WrapperGenerator.Tests/EmitterTests.cs | 40 +- tools/WrapperGenerator.Tests/NamingTests.cs | 4 +- .../SchemaPropertiesTests.cs | 517 +++++++++++++++--- .../WrapperGenerator.Tests/SpecShapeTests.cs | 142 +++++ tools/WrapperGenerator/CmdletEmitter.cs | 151 ++++- .../PowerShellWrapperGenerationService.cs | 90 ++- tools/WrapperGenerator/Program.cs | 13 +- tools/WrapperGenerator/README.md | 77 ++- tools/WrapperGenerator/SchemaProperties.cs | 500 +++++++++++++++-- tools/WrapperGenerator/Singularizer.cs | 2 +- tools/WrapperGenerator/StderrLogger.cs | 9 +- .../docs/body-property-binding.md | 241 ++++++++ .../edge-cases/body-binding-edge-cases.md | 156 ++++++ .../edge-cases/crosspath-merge-edge-cases.md | 0 .../edge-cases/kiota-alignment-edge-cases.md | 0 .../edge-cases/naming-edge-cases.md | 22 + 24 files changed, 2623 insertions(+), 213 deletions(-) create mode 100644 tools/Compare-WrapperOperationInventory.ps1 create mode 100644 tools/Measure-BodyPropertyCoverage.ps1 create mode 100644 tools/New-WrapperOutputManifest.ps1 create mode 100644 tools/Test-BodyBindingCoverage.ps1 create mode 100644 tools/WrapperGenerator.Tests/SpecShapeTests.cs create mode 100644 tools/WrapperGenerator/docs/body-property-binding.md create mode 100644 tools/WrapperGenerator/docs/edge-cases/body-binding-edge-cases.md rename tools/WrapperGenerator/{ => docs}/edge-cases/crosspath-merge-edge-cases.md (100%) rename tools/WrapperGenerator/{ => docs}/edge-cases/kiota-alignment-edge-cases.md (100%) rename tools/WrapperGenerator/{ => docs}/edge-cases/naming-edge-cases.md (88%) diff --git a/.gitignore b/.gitignore index 3e41c4492f0..0d734493bd1 100644 --- a/.gitignore +++ b/.gitignore @@ -252,6 +252,10 @@ Generated_Code/ # because we have git ;-) _UpgradeReport_Files/ Backup*/ +# ...but src/BackupRestore is a Graph service module, not a Visual Studio backup folder. The +# rule above excludes the directory itself, so git never descends into it; re-including the +# directory is what makes its generated output committable at all. +!src/BackupRestore/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ diff --git a/tools/Build-WrapperModule.ps1 b/tools/Build-WrapperModule.ps1 index 7630a2563ef..571994e82ce 100644 --- a/tools/Build-WrapperModule.ps1 +++ b/tools/Build-WrapperModule.ps1 @@ -23,9 +23,10 @@ Get-* dispatchers forward to the workers by name via InvokeCommand.InvokeScript, manifest that hides the workers breaks dispatch ("term not recognized"). Worker visibility needs its own dispatch design and is tracked in the module-wiring issue. -Everything is written under artifacts/ (gitignored); nothing this script produces is -committed. To check cmdlet-name parity for a built module, point the parity gate at its -cmdlets folder: +By default everything is written under artifacts/ (gitignored) for throwaway local runs. With +-IntoSource the same pipeline writes the committed layout under src///wrapper/, +where the client, the wrappers and the csproj live together so the folder builds standalone. +To check cmdlet-name parity for a built module, point the parity gate at its cmdlets folder: .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath artifacts\wrapper-modules\\src\Cmdlets .PARAMETER Module @@ -51,11 +52,19 @@ dotnet build configuration. Default: Debug. .PARAMETER SkipKiota Reuse the previously generated client (fast inner loop when only the wrappers changed). +.PARAMETER IntoSource +Write the committed layout under src///wrapper/ instead of artifacts/: +Client/ + Cmdlets/ + the csproj, self-contained so the folder builds on its own. This is how +the generated output is checked in; omit it for throwaway local builds. + .EXAMPLE .\tools\Build-WrapperModule.ps1 -Module Mail .EXAMPLE .\tools\Build-WrapperModule.ps1 -Module Mail,Calendar -ApiVersion v1.0 + +.EXAMPLE +.\tools\Build-WrapperModule.ps1 -Module Mail -IntoSource #> [CmdletBinding()] param( @@ -66,7 +75,8 @@ param( [string]$SpecRoot, [string]$OutputRoot, [string]$Configuration = 'Debug', - [switch]$SkipKiota + [switch]$SkipKiota, + [switch]$IntoSource ) $ErrorActionPreference = 'Stop' @@ -116,7 +126,17 @@ function Build-OneModule { $moduleName = "Microsoft.Graph.Wrapper.$Name" $clientNs = "Microsoft.Graph.PowerShell.$Name.Client" - $srcDir = Join-Path $OutputRoot "$Name\src" + # -IntoSource writes the committed layout: one self-contained project folder per module + # and API version, holding the kiota client, the wrappers, and the csproj that compiles + # both into one assembly. Everything under it is committable as-is (the module + # .gitignore blocks a csproj at the version-folder root, and still ignores bin/obj at + # any depth). Without the switch, output stays in artifacts/ for throwaway local runs. + $srcDir = if ($IntoSource) { + Join-Path $repoRoot "src\$Name\$ApiVersion\wrapper" + } + else { + Join-Path $OutputRoot "$Name\src" + } $clientDir = Join-Path $srcDir 'Client' $cmdletsDir = Join-Path $srcDir 'Cmdlets' New-Item -ItemType Directory -Force -Path $srcDir | Out-Null @@ -164,7 +184,7 @@ function Build-OneModule { $csprojPath = Join-Path $srcDir "$moduleName.csproj" $authCsprojRelative = [System.IO.Path]::GetRelativePath($srcDir, $authCsproj) -replace '/', '\' @" - + diff --git a/tools/Compare-WrapperCmdletNames.ps1 b/tools/Compare-WrapperCmdletNames.ps1 index d1a6b0a5c24..de2564b7088 100644 --- a/tools/Compare-WrapperCmdletNames.ps1 +++ b/tools/Compare-WrapperCmdletNames.ps1 @@ -13,7 +13,7 @@ emitted [Cmdlet(...)] name matches what the oracle says the published SDK calls operation. A small set of published names are known AutoRest defects the generator deliberately -corrects instead of reproducing (tools/WrapperGenerator/edge-cases/naming-edge-cases.md +corrects instead of reproducing (tools/WrapperGenerator/docs/edge-cases/naming-edge-cases.md is the catalog). Those are matched against the $deliberateCorrections table below and reported as [CORRECTED] rather than [MISMATCH]; they do not fail the gate. @@ -133,7 +133,7 @@ function Get-ModuleApiVersion { # Published names the generator deliberately corrects instead of reproducing. Each entry maps # the shipped (wrong) command to the corrected one the generator emits, and must have a matching -# entry in tools/WrapperGenerator/edge-cases/naming-edge-cases.md and a pinned naming test. The +# entry in tools/WrapperGenerator/docs/edge-cases/naming-edge-cases.md and a pinned naming test. The # gate reports these as [CORRECTED] instead of [MISMATCH] and does not fail on them. $deliberateCorrections = @{ # AutoRest inflected the trailing /whois segment to "Whoi"; the other 28 whois-family @@ -270,7 +270,7 @@ foreach ($module in $modules | Sort-Object Name) { $oracleCommand = $candidates | Select-Object -First 1 if ($deliberateCorrections[$oracleCommand] -eq $expectedCommand) { $moduleCorrected++ - $moduleCorrections += " [CORRECTED] $($file.Name): oracle ships '$oracleCommand'; generator deliberately emits '$expectedCommand' (see tools/WrapperGenerator/edge-cases/naming-edge-cases.md)." + $moduleCorrections += " [CORRECTED] $($file.Name): oracle ships '$oracleCommand'; generator deliberately emits '$expectedCommand' (see tools/WrapperGenerator/docs/edge-cases/naming-edge-cases.md)." } else { $moduleProblems += " [MISMATCH] $($file.Name): generated '$expectedCommand', oracle says '$oracleCommand' for $method $normalizedUri." diff --git a/tools/Compare-WrapperOperationInventory.ps1 b/tools/Compare-WrapperOperationInventory.ps1 new file mode 100644 index 00000000000..31e08476111 --- /dev/null +++ b/tools/Compare-WrapperOperationInventory.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS +Captures, and compares, the set of operations the generator turns into cmdlets. + +.DESCRIPTION +A change meant to affect only cmdlet PARAMETERS must not change which OPERATIONS generate. +Comparing filenames alone cannot show that: two operations could exchange ownership of a +cmdlet name and leave the same set of files behind. This records the full identity of each +emitted cmdlet - module, verb, noun, request path (the kiota builder chain, which is the +operation's path) and file - and diffs two snapshots on that tuple. + +Use -Baseline to record the current state before a change, then -Compare afterwards. + +.EXAMPLE +.\tools\Compare-WrapperOperationInventory.ps1 -Path artifacts\wrapper-modules -Baseline before.csv +.EXAMPLE +.\tools\Compare-WrapperOperationInventory.ps1 -Path artifacts\wrapper-modules -Baseline before.csv -Compare after.csv +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$Path, + [Parameter(Mandatory)] + [string]$Baseline, + [string]$Compare +) + +$ErrorActionPreference = 'Stop' + +$cmdletAttrPattern = '\[Cmdlet\(Verbs\w+\.(\w+),\s*"((?:\\.|[^"\\])*)"' +$builderPattern = 'client\.([A-Za-z0-9_\[\]\.]+?)\.(?:Get|Post|Patch|Delete|Put)Async' + +# Finds every Cmdlets folder under the root at any depth and takes the module name from the +# first segment below it, so the artifacts layout (/src/Cmdlets) and the committed one +# (//wrapper/Cmdlets) can be compared against each other. +function Get-Inventory([string]$root) { + $rootFull = (Resolve-Path $root).Path + $rows = [System.Collections.Generic.List[object]]::new() + foreach ($cmdletsDir in (Get-ChildItem $rootFull -Directory -Recurse -Filter 'Cmdlets')) { + $relative = $cmdletsDir.FullName.Substring($rootFull.Length).TrimStart('\', '/') + $module = ($relative -split '[\\/]')[0] + foreach ($file in Get-ChildItem $cmdletsDir.FullName -Filter '*.g.cs' -File) { + if ($file.Name -eq 'Shared.g.cs') { continue } + $text = Get-Content $file.FullName -Raw + $m = [regex]::Match($text, $cmdletAttrPattern) + if (-not $m.Success) { continue } + $b = [regex]::Match($text, $builderPattern) + $rows.Add([pscustomobject]@{ + Module = $module + Cmdlet = "$($m.Groups[1].Value)-$([regex]::Unescape($m.Groups[2].Value))" + Verb = $m.Groups[1].Value + RequestPath = if ($b.Success) { $b.Groups[1].Value } else { '(dispatcher)' } + File = $file.Name + }) + } + } + return $rows | Sort-Object Module, File +} + +$inventory = Get-Inventory $Path +# An empty inventory means the path or layout is wrong. Left unchecked it compares nothing +# against nothing and reports "unchanged" - a pass that proves the opposite of what it claims. +if ($inventory.Count -eq 0) { + Write-Error "No cmdlets found under '$Path'. Expected /src/Cmdlets or //wrapper/Cmdlets." + exit 2 +} + +if (-not $Compare) { + $inventory | Export-Csv $Baseline -NoTypeInformation + "baseline: $($inventory.Count) cmdlets -> $Baseline" + exit 0 +} + +$inventory | Export-Csv $Compare -NoTypeInformation +$before = Import-Csv $Baseline +$after = Import-Csv $Compare + +# Identity is the whole tuple, so an operation swapping which cmdlet/file it owns shows up as +# one removal plus one addition rather than as no change at all. +function Key($r) { "{0}|{1}|{2}|{3}" -f $r.Module, $r.Cmdlet, $r.RequestPath, $r.File } +# Filled by Add, and built inline rather than in a helper function, for two separate reasons: +# the HashSet(IEnumerable) constructor is ambiguous against +# HashSet(IEqualityComparer) when a side is empty, and returning a set FROM a function +# makes PowerShell enumerate it back into an Object[] - whose Contains is a linear scan, turning +# this comparison into ~n^2 string compares over ~10k identities. +$beforeKeys = [System.Collections.Generic.HashSet[string]]::new() +foreach ($r in $before) { [void]$beforeKeys.Add((Key $r)) } +$afterKeys = [System.Collections.Generic.HashSet[string]]::new() +foreach ($r in $after) { [void]$afterKeys.Add((Key $r)) } + +$added = @($afterKeys | Where-Object { -not $beforeKeys.Contains($_) }) +$removed = @($beforeKeys | Where-Object { -not $afterKeys.Contains($_) }) + +"before: $($before.Count) cmdlets" +"after: $($after.Count) cmdlets" +"added: $($added.Count)" +"removed: $($removed.Count)" +if ($added) { ""; "ADDED:"; $added | Select-Object -First 25 | ForEach-Object { " $_" } } +if ($removed) { ""; "REMOVED:"; $removed | Select-Object -First 25 | ForEach-Object { " $_" } } + +if ($added.Count -eq 0 -and $removed.Count -eq 0) { + ""; "operation inventory unchanged." + exit 0 +} +exit 1 diff --git a/tools/Measure-BodyPropertyCoverage.ps1 b/tools/Measure-BodyPropertyCoverage.ps1 new file mode 100644 index 00000000000..cf0afa6d289 --- /dev/null +++ b/tools/Measure-BodyPropertyCoverage.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS +Reports how request-body properties classify across every module: bound, excluded, or +unsupported and why. + +.DESCRIPTION +Runs the generator over each spec and reads the per-property diagnostics it emits, so the +numbers come from the same classifier that decides what gets bound - not a second +reimplementation that could disagree with it. + +Output is a per-shape rollup (which unsupported shapes are worth implementing next) and a +per-module CSV. Required-but-unbound properties are called out separately: those are the ones +that make a cmdlet unable to complete its request at all. + +.EXAMPLE +.\tools\Measure-BodyPropertyCoverage.ps1 +#> +[CmdletBinding()] +param( + [ValidateSet('v1.0', 'beta')] + [string]$ApiVersion = 'v1.0', + [string]$OutCsv +) + +$ErrorActionPreference = 'Stop' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +if (-not $OutCsv) { $OutCsv = Join-Path $repoRoot "artifacts\body-property-coverage.$ApiVersion.csv" } + +$specRoot = Join-Path $repoRoot "openApiDocs_KiotaCompat\$ApiVersion" +$generator = Join-Path $repoRoot 'tools\WrapperGenerator' +$scratch = Join-Path $repoRoot "artifacts\body-coverage-scratch" +New-Item -ItemType Directory -Force $scratch | Out-Null +New-Item -ItemType Directory -Force (Split-Path $OutCsv) | Out-Null + +$rows = [System.Collections.Generic.List[object]]::new() +$specs = @(Get-ChildItem "$specRoot\*.yml" | Sort-Object Name) +# A run over no specs would report "0 unbound" - a clean bill of health from having measured +# nothing, which is the failure mode this whole sweep exists to avoid. +if ($specs.Count -eq 0) { Write-Error "No specs found under '$specRoot'."; exit 2 } +$failedSpecs = [System.Collections.Generic.List[string]]::new() + +foreach ($spec in $specs) { + $module = $spec.BaseName + $out = Join-Path $scratch $module + Remove-Item $out -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force $out | Out-Null + + # Information level so the per-property diagnostics are emitted. + $log = & dotnet run --project $generator -c Release -- ` + -d $spec.FullName -o $out -n "Microsoft.Graph.PowerShell.$module.Client" --api-version $ApiVersion --log-level Information 2>&1 + # A module that failed to generate emits no diagnostics, so its properties would silently + # count as zero unbound and flatter the total. + if ($LASTEXITCODE -ne 0) { + $failedSpecs.Add("$module (exit $LASTEXITCODE)") + Write-Warning "$module : generation failed; excluded from the totals" + continue + } + + foreach ($line in $log) { + if ("$line" -match 'Unbound body property (?[^.]+)\.(?\S+): (?\w+) \(required=(?\w+)\)') { + $rows.Add([pscustomobject]@{ + Module = $module + Noun = $Matches.noun + Property = $Matches.prop + Shape = $Matches.shape + Required = [bool]::Parse($Matches.req) + }) + } + } + Write-Host ("{0,-34} unbound: {1}" -f $module, @($rows | Where-Object Module -eq $module).Count) +} + +# A CSV written from a partial sweep reads exactly like a complete one, so it is only produced +# when every spec generated. The population is stated beside the totals for the same reason. +if ($failedSpecs.Count -gt 0) { + Write-Error "$($failedSpecs.Count) of $($specs.Count) specs failed to generate: $($failedSpecs -join ', '). No CSV written - these totals would understate the unbound surface." + exit 1 +} +$rows | Export-Csv $OutCsv -NoTypeInformation + +# The generator reports a property per OPERATION, so an inherited property on a widely reused +# model repeats across every cmdlet that binds it. Both figures matter and mean different +# things: occurrences size the noise in a run, distinct identities size the actual work. +$identity = { "$($_.Module)|$($_.Noun)|$($_.Property)|$($_.Shape)" } +$distinct = @($rows | ForEach-Object $identity | Sort-Object -Unique) + +""; "=== unbound body properties by shape (occurrences / distinct) ===" +$rows | Group-Object Shape | Sort-Object Count -Descending | + Select-Object Count, Name, + @{n = 'Distinct'; e = { @($_.Group | ForEach-Object $identity | Sort-Object -Unique).Count } } | + Format-Table -AutoSize | Out-String -Width 80 + +"=== distinct property names per shape (top 8 each) ===" +foreach ($g in ($rows | Group-Object Shape | Sort-Object Count -Descending)) { + $names = ($g.Group | Select-Object -ExpandProperty Property -Unique | Select-Object -First 8) -join ', ' + " {0,-16} {1}" -f $g.Name, $names +} + +""; "specs generated: $($specs.Count) of $($specs.Count)" +"total unbound occurrences: $($rows.Count)" +"distinct module/noun/prop/shape: $($distinct.Count)" +# Graph marks almost nothing required in its schemas (the overwhelming majority of required +# blocks list only @odata.type), so this count is reported for completeness and is not +# evidence that nothing important is unbound. +"flagged required in the spec: $(@($rows | Where-Object Required -eq 'True').Count)" +"csv: $OutCsv" diff --git a/tools/New-WrapperOutputManifest.ps1 b/tools/New-WrapperOutputManifest.ps1 new file mode 100644 index 00000000000..0d852703055 --- /dev/null +++ b/tools/New-WrapperOutputManifest.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Writes a reviewable inventory of the committed wrapper output under src///wrapper/. + +.DESCRIPTION +The committed output is tens of thousands of generated files - far past what GitHub renders in +a diff and far past what anyone reads. This emits the summary a reviewer actually can read: +one CSV row per exported cmdlet (module, verb, noun, request path, source file) plus a +per-module rollup, so "what does this generator produce" and "what changed since last time" +are answerable from a diff of two small files instead of a diff of the tree. + +Cmdlet names come from the emitted [Cmdlet(VerbsX.Verb, "Noun")] attribute and the request +path from the emitted kiota builder chain - the generated source is the source of truth, so +the manifest cannot drift from what the module will actually export. + +Internal *_Get/*_List workers are listed with IsWorker = True: they are real emitted files but +not part of the surface a user calls, and separating them keeps the cmdlet count honest. (The +psd1 currently exports them anyway - the dispatcher resolves them by name at runtime - which is +a dispatch-design question tracked with the module-wiring work, not a manifest concern.) + +.EXAMPLE +.\tools\New-WrapperOutputManifest.ps1 +.EXAMPLE +.\tools\New-WrapperOutputManifest.ps1 -ApiVersion v1.0 +#> +[CmdletBinding()] +param( + [ValidateSet('v1.0', 'beta')] + [string]$ApiVersion = 'v1.0', + [string]$OutDir +) + +$ErrorActionPreference = 'Stop' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +# docs/ already holds versioned CSV inventories of the shipped surface +# (PowerShellBreakingChanges-V1.0.csv); this follows that placement and naming. +if (-not $OutDir) { $OutDir = Join-Path $repoRoot 'docs' } +$versionTag = if ($ApiVersion -eq 'v1.0') { 'V1.0' } else { 'Beta' } + +# Same attribute pattern the parity gate and Build-WrapperModule use. +$cmdletAttrPattern = '\[Cmdlet\(Verbs\w+\.(\w+),\s*"((?:\\.|[^"\\])*)"' +# The kiota chain the cmdlet calls, e.g. "client.Users[UserId].Messages.GetAsync()". +$builderPattern = 'client\.([A-Za-z0-9_\[\]\.]+?)\.(?:Get|Post|Patch|Delete|Put)Async' + +$rows = [System.Collections.Generic.List[object]]::new() +$moduleDirs = Get-ChildItem (Join-Path $repoRoot 'src') -Directory | + ForEach-Object { Join-Path $_.FullName "$ApiVersion\wrapper\Cmdlets" } | + Where-Object { Test-Path $_ } + +foreach ($dir in $moduleDirs) { + $module = (Get-Item $dir).Parent.Parent.Parent.Name + foreach ($file in Get-ChildItem $dir -Filter '*.g.cs' -File) { + if ($file.Name -eq 'Shared.g.cs') { continue } + $text = Get-Content $file.FullName -Raw + $m = [regex]::Match($text, $cmdletAttrPattern) + if (-not $m.Success) { continue } + $b = [regex]::Match($text, $builderPattern) + # A dispatcher makes no request itself - it forwards to its _Get/_List workers - so an + # absent builder chain identifies one rather than indicating a parse failure. + $isWorker = $file.Name -match '_(Get|List)\.g\.cs$' + $rows.Add([pscustomobject]@{ + Module = $module + ApiVersion = $ApiVersion + Cmdlet = "$($m.Groups[1].Value)-$([regex]::Unescape($m.Groups[2].Value))" + Verb = $m.Groups[1].Value + Noun = [regex]::Unescape($m.Groups[2].Value) + RequestPath = if ($b.Success) { $b.Groups[1].Value } elseif (-not $isWorker) { '(dispatcher)' } else { '' } + IsWorker = $isWorker + File = $file.Name + }) + } +} + +if ($rows.Count -eq 0) { throw "No committed wrapper output found for $ApiVersion under src/*/$ApiVersion/wrapper/Cmdlets." } + +$manifestPath = Join-Path $OutDir "WrapperCmdlets-$versionTag.csv" +$rows | Sort-Object Module, Cmdlet, File | Export-Csv $manifestPath -NoTypeInformation + +$summaryPath = Join-Path $OutDir "WrapperCmdlets-$versionTag-Summary.csv" +$rows | Group-Object Module | ForEach-Object { + $public = @($_.Group | Where-Object { -not $_.IsWorker }) + [pscustomobject]@{ + Module = $_.Name + Cmdlets = $public.Count + WorkerFiles = $_.Count - $public.Count + Get = @($public | Where-Object Verb -eq 'Get').Count + New = @($public | Where-Object Verb -eq 'New').Count + Update = @($public | Where-Object Verb -eq 'Update').Count + Remove = @($public | Where-Object Verb -eq 'Remove').Count + } +} | Sort-Object Cmdlets -Descending | Export-Csv $summaryPath -NoTypeInformation + +$publicTotal = @($rows | Where-Object { -not $_.IsWorker }).Count +"modules: $($rows | Group-Object Module | Measure-Object | Select-Object -ExpandProperty Count)" +"public cmdlets: $publicTotal" +"worker files: $($rows.Count - $publicTotal)" +"wrote $manifestPath" +"wrote $summaryPath" diff --git a/tools/Test-BodyBindingCoverage.ps1 b/tools/Test-BodyBindingCoverage.ps1 new file mode 100644 index 00000000000..7ac460c2e4a --- /dev/null +++ b/tools/Test-BodyBindingCoverage.ps1 @@ -0,0 +1,296 @@ +<# +.SYNOPSIS +Independent check that every settable Kiota request-body member is either bound by a cmdlet +parameter or accounted for by a named policy. + +.DESCRIPTION +Compilation proves that what we DO emit has the right CLR type - a wrong type name cannot +build. It cannot see what we FAIL to emit: a settable model member with no parameter, or a +parameter declared and never assigned, are both perfectly valid C#. This closes that gap. + +The invariant, per request-body model: + + settable kiota members == parameters with assignments + + properties excluded by a named policy + + properties reported as an unsupported shape + +Nothing here re-derives classification from the OpenAPI spec. The three inputs are produced +independently of each other: + + * the kiota client - generated by kiota, parsed here for its settable members and the + serialized (OpenAPI) name each one deserializes from + * the emitted cmdlet - parsed here for [Parameter] declarations and body.X = Y assignments + * the generator log - the classifier's own per-property exclusion/unsupported diagnostics + +A disagreement between them is a real defect in one of the three, which is the point. + +Failures reported: + MISSING a settable member with no parameter and no cited policy + NO-ASSIGNMENT a parameter that never assigns to the body + WRONG-TARGET an assignment to a member the model does not have + NO-PARAMETER an assignment reading an undeclared parameter + DUPLICATE two parameters assigning the same member + UNCITED a property excluded citing an unrecognised policy or shape + NO-MODEL a body type with no generated model file to check against + GENERATOR-FAILED generation failed, so its diagnostics cannot be trusted + +Modules whose spec or build output is absent are reported as skipped and counted, so a run +cannot look complete while silently covering less than it was asked to. + +.PARAMETER Module +Modules to check. Default: every module with generated cmdlets under -OutputRoot. + +.EXAMPLE +.\tools\Test-BodyBindingCoverage.ps1 -Module Users +.EXAMPLE +.\tools\Test-BodyBindingCoverage.ps1 +#> +[CmdletBinding()] +param( + [string[]]$Module, + [string]$OutputRoot, + [ValidateSet('v1.0', 'beta')] + [string]$ApiVersion = 'v1.0', + [string]$SpecRoot +) + +$ErrorActionPreference = 'Stop' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +if (-not $OutputRoot) { $OutputRoot = Join-Path $repoRoot 'artifacts\wrapper-modules' } +if (-not $SpecRoot) { $SpecRoot = Join-Path $repoRoot 'openApiDocs_KiotaCompat' } +$OutputRoot = (Resolve-Path -LiteralPath $OutputRoot).Path +$generator = Join-Path $repoRoot 'tools\WrapperGenerator' + +if (-not $Module) { + $Module = @(Get-ChildItem $OutputRoot -Directory | + Where-Object { Test-Path (Join-Path $_.FullName 'src\Cmdlets') } | + ForEach-Object { $_.Name } | Sort-Object) +} +if (-not $Module) { Write-Error "No modules found under '$OutputRoot'."; exit 2 } + +# --- kiota model members ----------------------------------------------------------------- +# The deserializer map is the authority for which OpenAPI name feeds which member: +# { "displayName", n => { DisplayName = n.GetStringValue(); } }, +# Reading it avoids re-deriving kiota's name-cleaning rules (underscores, Prop suffixing) +# here, which would just be a second implementation that could drift from the first. +$deserializerEntry = '\{\s*"(?[^"]+)"\s*,\s*n\s*=>\s*\{\s*(?\w+)\s*=' + +# Members are inherited: Message declares Subject but gets ChangeKey from OutlookItem and +# CreatedDateTime from Entity, each in its own file with its own deserializer map. Walking the +# base chain is required or every inherited assignment looks like it targets a member that does +# not exist. +# relativeName is everything after ".Models." - "CallRecords.Participant", not "Participant". +# Kiota nests a dotted schema name as a sub-namespace and a sub-folder, and several of those +# nested models share a simple name with a different model at the root +# (Models/Participant.cs vs Models/CallRecords/Participant.cs). Matching on the simple name +# silently compares a cmdlet against the wrong model, which is worse than not finding one. +function Get-ModelMembers([string]$modelsDir, [string]$relativeName, [System.Collections.Generic.HashSet[string]]$visited) { + if ($null -eq $visited) { $visited = [System.Collections.Generic.HashSet[string]]::new() } + if (-not $visited.Add($relativeName)) { return @{} } # defensive: never loop on a cyclic chain + + $segments = $relativeName -split '\.' + $typeName = $segments[-1] + $file = Join-Path $modelsDir ((($segments) -join [IO.Path]::DirectorySeparatorChar) + '.cs') + if (-not (Test-Path $file)) { return $null } + $text = Get-Content $file -Raw + $folder = if ($segments.Count -gt 1) { ($segments[0..($segments.Count - 2)] -join '.') + '.' } else { '' } + + $map = @{} + # The base type is fully qualified in the declaration; keep whatever sits after ".Models." + # so a nested base resolves to its own folder rather than the root. + $baseMatch = [regex]::Match($text, "public partial class $([regex]::Escape($typeName))\s*:\s*global::[A-Za-z0-9_.]*?\.Models\.(?[A-Za-z0-9_.]+)\s*,") + if ($baseMatch.Success) { + $inherited = Get-ModelMembers $modelsDir $baseMatch.Groups['base'].Value $visited + if ($null -ne $inherited) { + foreach ($k in $inherited.Keys) { $map[$k] = $inherited[$k] } + } + } + foreach ($m in [regex]::Matches($text, $deserializerEntry)) { + $map[$m.Groups['member'].Value] = $m.Groups['json'].Value + } + return $map +} + +# --- run the generator once per module to collect its diagnostics -------------------------- +$results = [System.Collections.Generic.List[object]]::new() +$failures = [System.Collections.Generic.List[object]]::new() + +# Only these may account for an unbound member. An exclusion naming anything else means the +# generator emitted a policy this check does not know about, which must fail rather than be +# accepted as a citation. +$knownPolicies = @( + 'ServerAssignedId', 'ODataControlData', 'KiotaAdditionalData', 'ReadOnlySchema', 'NavigationProperty' +) +$knownShapes = @('InlineEnum', 'UnknownFormat', 'InlineObject', 'Union', 'Dictionary', 'Unresolvable') + +$skipped = [System.Collections.Generic.List[string]]::new() + +foreach ($name in $Module) { + $spec = Join-Path $SpecRoot "$ApiVersion\$name.yml" + if (-not (Test-Path $spec)) { $spec = Join-Path $repoRoot "openApiDocs\$ApiVersion\$name.yml" } + if (-not (Test-Path $spec)) { $skipped.Add("$name (no spec)"); continue } + + $cmdletsDir = Join-Path $OutputRoot "$name\src\Cmdlets" + $modelsDir = Join-Path $OutputRoot "$name\src\Client\Models" + if (-not (Test-Path $cmdletsDir) -or -not (Test-Path $modelsDir)) { $skipped.Add("$name (not built)"); continue } + + $log = & dotnet run --project $generator -c Release -- ` + -d $spec -o (Join-Path $env:TEMP "binding-oracle-$name") -n "Microsoft.Graph.PowerShell.$name.Client" ` + --api-version $ApiVersion --log-level Information 2>&1 + # A failed generation produces no diagnostics, which would make every unbound member look + # like an uncited omission - or worse, make a module with no cmdlets look clean. + if ($LASTEXITCODE -ne 0) { + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = '(generation)'; Kind = 'GENERATOR-FAILED'; Detail = "exit $LASTEXITCODE; diagnostics unusable" }) + continue + } + + # noun -> set of OpenAPI property names the classifier deliberately did not bind. + # The two diagnostics are parsed separately and validated against DIFFERENT vocabularies: a + # policy exclusion and an unsupported shape are different claims, and one accepted set would + # let "Excluded ...: Untyped" or "Unbound ...: ServerAssignedId" pass as a citation. + $accounted = @{} + foreach ($line in $log) { + $kind = $null + $m = [regex]::Match("$line", 'Excluded body property (?[^.]+)\.(?\S+): (?\S+)') + if ($m.Success) { $kind = 'Excluded'; $allowed = $knownPolicies } + else { + $m = [regex]::Match("$line", 'Unbound body property (?[^.]+)\.(?\S+): (?\S+)') + if ($m.Success) { $kind = 'Unbound'; $allowed = $knownShapes } + } + if (-not $kind) { continue } + + $reason = $m.Groups['reason'].Value.Trim() + if ($reason -notin $allowed) { + $expected = if ($kind -eq 'Excluded') { 'an ExclusionPolicy' } else { 'an UnsupportedShape' } + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = '(diagnostics)'; Kind = 'UNCITED'; Detail = "$kind $($m.Groups['prop'].Value) cited '$reason'; expected $expected" }) + continue + } + $noun = $m.Groups['noun'].Value + if (-not $accounted.ContainsKey($noun)) { $accounted[$noun] = [System.Collections.Generic.HashSet[string]]::new() } + [void]$accounted[$noun].Add($m.Groups['prop'].Value) + } + + foreach ($file in Get-ChildItem $cmdletsDir -Filter '*.g.cs' -File) { + if ($file.Name -notmatch '^(New|Update)Mg') { continue } + $text = Get-Content $file.FullName -Raw + + # -match populates $Matches; -notmatch does not, so each pattern is matched explicitly. + $bodyMatch = [regex]::Match($text, 'var body = new ([A-Za-z0-9_.]+)\(\);') + if (-not $bodyMatch.Success) { continue } + $entityType = $bodyMatch.Groups[1].Value + # Keep the sub-namespace: "...Models.CallRecords.Participant" -> "CallRecords.Participant". + $modelsMarker = '.Models.' + $markerAt = $entityType.IndexOf($modelsMarker) + $relativeName = if ($markerAt -ge 0) { $entityType.Substring($markerAt + $modelsMarker.Length) } else { $entityType } + $simpleName = $relativeName.Substring($relativeName.LastIndexOf('.') + 1) + + $members = Get-ModelMembers $modelsDir $relativeName $null + if ($null -eq $members) { + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = $file.BaseName; Kind = 'NO-MODEL'; Detail = "no generated model file for $relativeName" }) + continue + } + + # The noun is used verbatim: the generator's diagnostics key on the same prefixed noun + # (MgUserMailFolder), so stripping the prefix here would match nothing and report every + # policy exclusion as an omission. + $cmdletMatch = [regex]::Match($text, '\[Cmdlet\(Verbs\w+\.(\w+),\s*"([^"]+)"') + if (-not $cmdletMatch.Success) { continue } + $noun = $cmdletMatch.Groups[2].Value + + # emitted parameters and the member each one assigns + $parameters = @([regex]::Matches($text, '(?m)^\s+public\s+[^\r\n]+?\s+(\w+)\s*\{\s*get;\s*set;\s*\}') | + ForEach-Object { $_.Groups[1].Value }) + # A schema-less property is assigned in two steps, so the right-hand side of the + # assignment is a local rather than the parameter: + # var untypedX = UntypedValue.From(X); + # if (untypedX is not null) body.X = untypedX; + # Mapping the local back to its parameter keeps the assignment attributable; without it + # the parameter looks unassigned and the local looks undeclared. + $localToParam = @{} + foreach ($c in [regex]::Matches($text, 'var\s+(?\w+)\s*=\s*UntypedValue\.From\((?\w+)\)')) { + $localToParam[$c.Groups['local'].Value] = $c.Groups['param'].Value + } + + $assignments = @{} + $assignedParams = [System.Collections.Generic.HashSet[string]]::new() + foreach ($a in [regex]::Matches($text, '(?m)^\s+body\.(?\w+)\s*=\s*(?\w+)')) { + $member = $a.Groups['member'].Value + $source = $a.Groups['param'].Value + if ($localToParam.ContainsKey($source)) { $source = $localToParam[$source] } + if ($assignments.ContainsKey($member)) { + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = $file.BaseName; Kind = 'DUPLICATE'; Detail = "two parameters assign body.$member" }) + } + $assignments[$member] = $source + [void]$assignedParams.Add($source) + } + + # A parameter that never reaches the body is silently inert: it binds, the user supplies + # a value, and the request goes out without it. The compiler is perfectly happy with it. + # Parameters that legitimately do not assign are identified by how they are emitted, not + # by a list of names: a path id carries an "= string.Empty" initializer, a header param + # is added to requestConfiguration.Headers, and AccessToken/Headers are the shared + # plumbing every cmdlet declares. + $pathParams = @([regex]::Matches($text, '(?m)^\s+public\s+string\s+(\w+)\s*\{\s*get;\s*set;\s*\}\s*=\s*string\.Empty;') | + ForEach-Object { $_.Groups[1].Value }) + $headerParams = @([regex]::Matches($text, 'requestConfiguration\.Headers\.Add\("[^"]*",\s*(\w+)!') | + ForEach-Object { $_.Groups[1].Value }) + $nonBody = [System.Collections.Generic.HashSet[string]]::new([string[]](@('AccessToken', 'Headers') + $pathParams + $headerParams)) + foreach ($p in $parameters) { + if ($assignedParams.Contains($p) -or $nonBody.Contains($p)) { continue } + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = $file.BaseName; Kind = 'NO-ASSIGNMENT'; Detail = "-$p is declared but never assigned to the body" }) + } + + foreach ($member in $assignments.Keys) { + if (-not $members.ContainsKey($member)) { + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = $file.BaseName; Kind = 'WRONG-TARGET'; Detail = "body.$member is not a member of $simpleName" }) + } + elseif ($assignments[$member] -notin $parameters) { + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = $file.BaseName; Kind = 'NO-PARAMETER'; Detail = "body.$member reads undeclared $($assignments[$member])" }) + } + } + + # every settable member must be assigned, or named in the classifier's diagnostics + # Plain assignment, not an if-expression: an empty HashSet returned through the pipeline + # enumerates to nothing and the variable lands as $null. + $cited = [System.Collections.Generic.HashSet[string]]::new() + if ($accounted.ContainsKey($noun)) { $cited = $accounted[$noun] } + foreach ($member in $members.Keys) { + if ($assignments.ContainsKey($member)) { continue } + if ($cited.Contains($members[$member])) { continue } + $failures.Add([pscustomobject]@{ Module = $name; Cmdlet = $file.BaseName; Kind = 'MISSING'; Detail = "$simpleName.$member ('$($members[$member])') has no parameter and no cited policy" }) + } + + $results.Add([pscustomobject]@{ Module = $name; Cmdlet = $file.BaseName; Members = $members.Count; Assigned = $assignments.Count }) + } + Write-Host ("{0,-32} cmdlets {1,5} members {2,6} assigned {3,6}" -f $name, + @($results | Where-Object Module -eq $name).Count, + (($results | Where-Object Module -eq $name | Measure-Object Members -Sum).Sum), + (($results | Where-Object Module -eq $name | Measure-Object Assigned -Sum).Sum)) +} + +if ($results.Count -eq 0) { Write-Error 'No New/Update cmdlets were examined; the oracle proved nothing.'; exit 2 } + +$examinedModules = @($results | Select-Object -ExpandProperty Module -Unique) +# A module with no New/Update cmdlets has nothing for this oracle to check. Naming those +# explicitly keeps the population reconcilable without the reader subtracting two numbers. +# Compared against an explicit name list: a -match against an empty collection yields an empty +# array, which is falsy, so the filter would silently select nothing. +$skippedNames = @($skipped | ForEach-Object { ($_ -split ' ')[0] }) +$noBodyCmdlets = @($Module | Where-Object { $_ -notin $examinedModules -and $_ -notin $skippedNames }) +"" +"modules requested : $($Module.Count)" +"modules examined : $($examinedModules.Count)" +"modules with no New/Update: $($noBodyCmdlets.Count)$(if ($noBodyCmdlets.Count) { " -> $($noBodyCmdlets -join ', ')" })" +"modules skipped : $($skipped.Count)$(if ($skipped.Count) { " -> $($skipped -join ', ')" })" +"cmdlets examined : $($results.Count)" +"model members : $(($results | Measure-Object Members -Sum).Sum)" +"bound by a param : $(($results | Measure-Object Assigned -Sum).Sum)" +"failures : $($failures.Count)" +if ($failures.Count -gt 0) { + "" + $failures | Group-Object Kind | Sort-Object Count -Descending | Select-Object Count, Name | Format-Table -AutoSize | Out-String -Width 60 + $failures | Select-Object -First 25 | Format-Table Module, Cmdlet, Kind, Detail -AutoSize | Out-String -Width 200 + exit 1 +} +"binding coverage verified: every settable member is bound or cited." +exit 0 diff --git a/tools/Test-WrapperModule.ps1 b/tools/Test-WrapperModule.ps1 index 7460f4f2c60..0a19dd41543 100644 --- a/tools/Test-WrapperModule.ps1 +++ b/tools/Test-WrapperModule.ps1 @@ -8,6 +8,12 @@ Each module is tested in a CHILD pwsh process — a fresh process per module, be assemblies cannot be unloaded and Import-Module silently no-ops when a same-name module is already loaded. Checks, per module: + 0. the binary is not stale - the dll is compared against every compiled + input under src (the kiota client in Client/ + as well as Cmdlets/ and the csproj) and a + binary older than any of them is refused, + because every check below would pass against + a module built before the change under test 1. Import-Module succeeds - the user's first experience 2. exported cmdlet count == manifest count - nothing silently dropped at load 3. no orphan workers - every *_Get/*_List worker has its public @@ -16,8 +22,22 @@ already loaded. Checks, per module: PASS = NoGraphSession error (the call flowed dispatcher -> worker -> auth path) FAIL = CommandNotFound (dispatcher->worker forwarding broken: the manifest visibility trap) or any other unexpected error id + 5. each bound shape accepts the value a person would actually type, asserted against the + real compiled types rather than assumed: + complex - a model-typed parameter accepts a hashtable + enum - an enum-typed parameter accepts its own member name as a string + scalar - DateTimeOffset/Guid accept a string; kiota's Date/Time accept a [datetime] + (they have NO string conversion), reported as OK(n) where n is how many + cases the module actually exercised, so an empty pass is visible + untyped - 19 cases run through the module's OWN compiled UntypedValue helper, reached + by reflection so this gate cannot drift from a copy of the converter: every + numeric type, string, boolean, PSObject unwrapping, object, array, nesting, + nested-null drop, null-element drop, empty-object omission, and the throw on + an unsupported type. The helper is emitted into every module, so a missing + helper is a failure, never n/a Modules with no paired list+item GETs have no dispatcher; check 4 reports n/a for them. +A shape a module never binds reports n/a for that part of check 5, except untyped. .PARAMETER Module One or more module names previously built by Build-WrapperModule.ps1. @@ -43,6 +63,10 @@ $ErrorActionPreference = 'Stop' $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path if (-not $OutputRoot) { $OutputRoot = Join-Path $repoRoot 'artifacts\wrapper-modules' } +# The psd1 path is embedded in a script run by a CHILD process with its own working directory, +# so a relative -OutputRoot would resolve there and Import-Module would fail with a confusing +# "module not found" rather than a path error. +$OutputRoot = (Resolve-Path -LiteralPath $OutputRoot).Path function Test-OneModule { param([string]$Name) @@ -51,13 +75,37 @@ function Test-OneModule { $psd1 = Join-Path $OutputRoot "$Name\src\bin\$Configuration\net10.0\$moduleName.psd1" $result = [pscustomobject]@{ Module = $Name; Pass = $false; Exported = 0; ManifestCount = 0 - OrphanWorkers = 0; Dispatcher = ''; ErrorId = ''; Detail = '' + OrphanWorkers = 0; Dispatcher = ''; ErrorId = ''; ComplexBinding = ''; EnumBinding = ''; ScalarBinding = '' + UntypedBinding = ''; Detail = '' } if (-not (Test-Path $psd1)) { $result.Detail = "not built: $psd1 missing (run Build-WrapperModule.ps1 first)" return $result } + + # A binary older than the sources it was built from passes every check below while proving + # nothing about the current generator. This gate loads whatever is on disk, so staleness is + # invisible unless it is refused here: the build and test defaults can drift apart, and a + # module last built under a different configuration is silently days old. + # + # Every compiled input counts, not just the cmdlets. A module is emitted sources plus the + # kiota client under Client/, and a regenerated client with an unchanged cmdlet is exactly + # the case where a parameter's CLR type moves out from under the assignment - so watching + # Cmdlets/ alone would miss the change most likely to invalidate a runtime result. + $dll = Join-Path $OutputRoot "$Name\src\bin\$Configuration\net10.0\$moduleName.dll" + $inputs = @(Get-ChildItem -Path (Join-Path $OutputRoot "$Name\src") -Recurse -File -Include *.cs, *.csproj -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '\\(bin|obj)\\' }) + if ((Test-Path $dll) -and $inputs) { + $newest = ($inputs | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1) + $builtAt = (Get-Item $dll).LastWriteTimeUtc + if ($builtAt -lt $newest.LastWriteTimeUtc) { + $rel = $newest.FullName.Substring((Join-Path $OutputRoot "$Name\src").Length).TrimStart('\') + $result.Detail = "stale binary: $Configuration dll built $($builtAt.ToString('MM-dd HH:mm')) predates $rel ($($newest.LastWriteTimeUtc.ToString('MM-dd HH:mm'))); rebuild with -Configuration $Configuration" + return $result + } + } + $result.ManifestCount = (Import-PowerShellDataFile -Path $psd1).CmdletsToExport.Count # The child prints exactly one JSON line; everything else it may write is noise. @@ -83,11 +131,166 @@ if (`$dispatcher) { `$errorId = `$_.FullyQualifiedErrorId } } +# A model-typed parameter must accept a hashtable: that conversion is what makes +# -PasswordProfile @{ Password = '...' } work. Find one on any New-/Update- cmdlet and +# convert an empty hashtable to it; failure means typed binding is unusable from the shell. +`$complexBinding = 'N/A' +# Must be a model CLASS, not an enum: referenced enums are also .Client.Models.* types, and +# converting a hashtable to one is meaningless. Enums are covered by their own case below. +`$typed = `$cmds | + Where-Object { `$_.Name -like 'New-*' -or `$_.Name -like 'Update-*' } | + ForEach-Object { `$_.Parameters.Values } | + Where-Object { + `$_.ParameterType.FullName -like '*.Client.Models.*' -and -not `$_.ParameterType.IsArray -and + -not `$_.ParameterType.IsEnum -and -not ([System.Nullable]::GetUnderlyingType(`$_.ParameterType)) + } | + Select-Object -First 1 +if (`$typed) { + try { + `$converted = [System.Management.Automation.LanguagePrimitives]::ConvertTo(@{}, `$typed.ParameterType) + `$complexBinding = if (`$converted -and `$converted.GetType() -eq `$typed.ParameterType) { 'OK' } else { 'WRONG-TYPE' } + } + catch { + `$complexBinding = "FAILED: `$(`$_.Exception.Message)" + } +} + +# A referenced enum binds from the string a person would type. +`$enumBinding = 'N/A' +`$enumParam = `$cmds | + Where-Object { `$_.Name -like 'New-*' -or `$_.Name -like 'Update-*' } | + ForEach-Object { `$_.Parameters.Values } | + Where-Object { + `$u = [System.Nullable]::GetUnderlyingType(`$_.ParameterType) + `$u -and `$u.IsEnum + } | Select-Object -First 1 +if (`$enumParam) { + `$u = [System.Nullable]::GetUnderlyingType(`$enumParam.ParameterType) + `$sample = ([enum]::GetNames(`$u) | Select-Object -First 1) + try { + `$v = [System.Management.Automation.LanguagePrimitives]::ConvertTo(`$sample, `$enumParam.ParameterType) + `$enumBinding = if ("`$v" -eq `$sample) { 'OK' } else { "WRONG-VALUE(`$v)" } + } + catch { `$enumBinding = "FAILED: -`$(`$enumParam.Name)" } +} +# Scalar shapes are bound from a value a person would plausibly type. Kiota's Date and Time are +# the sharp edge: they are structs with no string conversion, so they take a [datetime] +# (what Get-Date returns) and binding a string fails. Pinning that here keeps the documented +# input contract honest - the parameter compiles either way, so only a runtime check can tell. +`$scalarBinding = 'N/A' +`$scalarCases = @{ + 'System.DateTimeOffset' = '2001-04-05T00:00:00Z' + 'System.Guid' = '00000000-0000-0000-0000-000000000000' + 'Microsoft.Kiota.Abstractions.Date' = [datetime]'2026-12-31' + 'Microsoft.Kiota.Abstractions.Time' = [datetime]'2026-12-31T14:30:00' +} +`$bodyParams = @(`$cmds | + Where-Object { `$_.Name -like 'New-*' -or `$_.Name -like 'Update-*' } | + ForEach-Object { `$_.Parameters.Values }) +`$scalarFailures = @() +`$scalarExercised = 0 +foreach (`$typeName in `$scalarCases.Keys) { + `$p = `$bodyParams | Where-Object { + `$u = [System.Nullable]::GetUnderlyingType(`$_.ParameterType) + `$u -and `$u.FullName -eq `$typeName + } | Select-Object -First 1 + if (-not `$p) { continue } + `$scalarExercised++ + try { + `$null = [System.Management.Automation.LanguagePrimitives]::ConvertTo(`$scalarCases[`$typeName], `$p.ParameterType) + } + catch { + `$scalarFailures += "-`$(`$p.Name) rejects `$typeName input" + } +} +# Reporting OK when no case matched would be a pass that proves nothing, so the count of +# cases actually exercised is carried in the result instead of being assumed. +if (`$scalarFailures) { `$scalarBinding = "FAILED: `$(`$scalarFailures -join ', ')" } +elseif (`$scalarExercised -gt 0) { `$scalarBinding = "OK(`$scalarExercised)" } + +# The schema-less converter is the one piece of emitted logic no compiler can check: every +# branch produces a UntypedNode, so a wrong branch sends a value the caller never wrote and +# still builds. The matrix runs the compiled helper inside the module under test - reached by +# reflection because it is internal - so it cannot drift from a copy kept in this script. +# UntypedValue is emitted into every module, so a module that cannot produce it is a failure, +# never N/A. +`$untypedBinding = 'NOT-FOUND' +`$untypedType = `$null +try { + `$impl = `$cmds | Where-Object { `$_.CommandType -eq 'Cmdlet' } | Select-Object -First 1 + `$untypedType = @(`$impl.ImplementingType.Assembly.GetTypes() | + Where-Object { `$_.Name -eq 'UntypedValue' })[0] +} +catch { `$untypedType = `$null } +if (`$untypedType) { + `$from = `$untypedType.GetMethod('From', [Reflection.BindingFlags]'Public,Static') + # Expect: node type name; '' means the property is omitted; 'THROW' means refused. + `$untypedCases = @( + @{ N = 'string'; V = 'hello'; T = 'UntypedString'; Val = 'hello' } + @{ N = 'boolean'; V = `$true; T = 'UntypedBoolean'; Val = 'True' } + @{ N = 'int32'; V = [int]42; T = 'UntypedInteger'; Val = '42' } + @{ N = 'int64'; V = [long]9000000000; T = 'UntypedLong'; Val = '9000000000' } + @{ N = 'float'; V = [float]1.5; T = 'UntypedFloat'; Val = '1.5' } + @{ N = 'double'; V = [double]2.5; T = 'UntypedDouble'; Val = '2.5' } + @{ N = 'decimal'; V = [decimal]3.5; T = 'UntypedDecimal'; Val = '3.5' } + @{ N = 'unsigned byte'; V = [byte]7; T = 'UntypedInteger'; Val = '7' } + @{ N = 'unsigned int'; V = [uint32]8; T = 'UntypedInteger'; Val = '8' } + @{ N = 'PSObject wrapper unwrapped'; V = [psobject]::AsPSObject('wrapped'); T = 'UntypedString'; Val = 'wrapped' } + @{ N = 'hashtable'; V = @{ a = 'x' }; T = 'UntypedObject'; Count = 1 } + @{ N = 'nested hashtable'; V = @{ o = @{ i = 'x' } }; T = 'UntypedObject'; Count = 1 } + @{ N = 'array'; V = @(1, 2); T = 'UntypedArray'; Count = 2 } + @{ N = 'null omitted'; V = `$null; T = '' } + @{ N = 'empty object omitted'; V = @{}; T = '' } + @{ N = 'all-null object omitted'; V = @{ a = `$null }; T = '' } + @{ N = 'nested null dropped, sibling kept'; V = @{ a = 'x'; b = `$null }; T = 'UntypedObject'; Count = 1 } + @{ N = 'null array element dropped'; V = @(1, `$null, 2); T = 'UntypedArray'; Count = 2 } + @{ N = 'unsupported type refused'; V = { 1 }; T = 'THROW' } + ) + `$untypedFailures = @() + foreach (`$case in `$untypedCases) { + # A one-element object[] built by hand: @(`$v) unrolls an array argument into the wrong arity. + `$callArgs = New-Object object[] 1 + `$callArgs[0] = `$case.V + `$threw = `$false + `$node = `$null + try { `$node = `$from.Invoke(`$null, `$callArgs) } + catch { `$threw = `$true } + + if (`$case.T -eq 'THROW') { + if (-not `$threw) { `$untypedFailures += "`$(`$case.N): accepted" } + continue + } + if (`$threw) { `$untypedFailures += "`$(`$case.N): threw"; continue } + if (`$case.T -eq '') { + if (`$null -ne `$node) { `$untypedFailures += "`$(`$case.N): sent `$(`$node.GetType().Name)" } + continue + } + if (`$null -eq `$node) { `$untypedFailures += "`$(`$case.N): omitted"; continue } + if (`$node.GetType().Name -ne `$case.T) { + `$untypedFailures += "`$(`$case.N): `$(`$node.GetType().Name) not `$(`$case.T)" + continue + } + if (`$case.ContainsKey('Val') -and "`$(`$node.GetValue())" -ne `$case.Val) { + `$untypedFailures += "`$(`$case.N): value `$(`$node.GetValue()) not `$(`$case.Val)" + } + if (`$case.ContainsKey('Count')) { + `$actual = @(`$node.GetValue()).Count + if (`$actual -ne `$case.Count) { `$untypedFailures += "`$(`$case.N): `$actual members not `$(`$case.Count)" } + } + } + `$untypedBinding = if (`$untypedFailures) { "FAILED: `$(`$untypedFailures -join '; ')" } + else { "OK(`$(`$untypedCases.Count))" } +} + [pscustomobject]@{ Exported = `$cmds.Count OrphanWorkers = `$orphans.Count Dispatcher = if (`$dispatcher) { `$dispatcher.Name } else { '' } ErrorId = `$errorId + ComplexBinding = `$complexBinding + EnumBinding = `$enumBinding + ScalarBinding = `$scalarBinding + UntypedBinding = `$untypedBinding } | ConvertTo-Json -Compress "@ @@ -106,6 +309,10 @@ if (`$dispatcher) { $result.OrphanWorkers = $r.OrphanWorkers $result.Dispatcher = $r.Dispatcher $result.ErrorId = $r.ErrorId + $result.ComplexBinding = $r.ComplexBinding + $result.EnumBinding = $r.EnumBinding + $result.ScalarBinding = $r.ScalarBinding + $result.UntypedBinding = $r.UntypedBinding if ($r.Exported -ne $result.ManifestCount) { $result.Detail = "exported $($r.Exported) != manifest $($result.ManifestCount)" @@ -120,6 +327,20 @@ if (`$dispatcher) { "unexpected error id: $($r.ErrorId)" } } + elseif ($r.ComplexBinding -notin @('OK', 'N/A')) { + $result.Detail = "complex parameter does not accept a hashtable: $($r.ComplexBinding)" + } + elseif ($r.EnumBinding -notin @('OK', 'N/A')) { + $result.Detail = "enum parameter does not accept its own member name: $($r.EnumBinding)" + } + elseif ($r.ScalarBinding -ne 'N/A' -and $r.ScalarBinding -notlike 'OK(*') { + $result.Detail = "scalar parameter rejects its documented input: $($r.ScalarBinding)" + } + # No N/A escape: UntypedValue is emitted into every module, so a missing helper or a zero + # count is a failure rather than a module that had nothing to test. + elseif ($r.UntypedBinding -notlike 'OK(*') { + $result.Detail = "schema-less conversion is wrong: $($r.UntypedBinding)" + } else { $result.Pass = $true } @@ -130,7 +351,7 @@ $results = foreach ($name in $Module) { Write-Host "=== $name ===" -ForegroundColor Cyan $r = Test-OneModule -Name $name if ($r.Pass) { - Write-Host " PASS: $($r.Exported) cmdlets; dispatcher $($r.Dispatcher) -> $($r.ErrorId)" -ForegroundColor Green + Write-Host " PASS: $($r.Exported) cmdlets; dispatcher $($r.Dispatcher) -> $($r.ErrorId); complex $($r.ComplexBinding); enum $($r.EnumBinding); scalar $($r.ScalarBinding); untyped $($r.UntypedBinding)" -ForegroundColor Green } else { Write-Host " FAIL: $($r.Detail)" -ForegroundColor Yellow diff --git a/tools/WrapperGenerator.Tests/EmitterTests.cs b/tools/WrapperGenerator.Tests/EmitterTests.cs index 24e8dc174b9..fa906fe73d0 100644 --- a/tools/WrapperGenerator.Tests/EmitterTests.cs +++ b/tools/WrapperGenerator.Tests/EmitterTests.cs @@ -32,17 +32,49 @@ public void DispatcherRethrowsTheWorkersOriginalErrorRecord() public void EmitsSuffixedParameterButAssignsRealModelProperty() { var naming = Naming.Resolve(new OperationInfo(HttpMethod.Patch, "/devices/{device-id}")); - var properties = SchemaProperties.ResolveParameterNameCollisions( + var (properties, _, _) = SchemaProperties.ResolveParameterNameCollisions( new[] { new CmdletProperty("deviceId", "DeviceId", "string", IsArray: false) }, + [], [], naming.PathParamNames); - var source = CmdletEmitter.EmitUpdate(naming, new EmitContext("Test.Client"), "Device", properties, hasPasswordProfile: false); + var source = CmdletEmitter.EmitUpdate(naming, new EmitContext("Test.Client"), "Device", properties, [], []); Assert.Contains("public string? DeviceId1 { get; set; }", source); Assert.Contains("body.DeviceId = DeviceId1;", source); Assert.Contains("IsParameterBound(nameof(DeviceId1))", source); } + // A complex body property binds as its kiota model type, fully qualified, and assigns + // straight to the model property. This is what lets a caller write + // New-MgUser -PasswordProfile @{ Password = '...' } - PowerShell converts the hashtable + // to the model on binding. Arrays land as T[] and convert with ToList() like scalar arrays. + [Fact] + public void EmitsComplexPropertyAsTypedModelParameter() + { + var naming = Naming.Resolve(new OperationInfo(HttpMethod.Post, "/users")); + var complex = new[] + { + new ComplexParameter("PasswordProfile", "PasswordProfile", "Test.Client.Models.PasswordProfile", IsArray: false, IsEnum: false), + new ComplexParameter("AssignedLicenses", "AssignedLicenses", "Test.Client.Models.AssignedLicense", IsArray: true, IsEnum: false), + // An enum collection needs nullable elements to assign to kiota's List. + new ComplexParameter("Roles", "Roles", "Test.Client.Models.RoleType", IsArray: true, IsEnum: true), + }; + + var source = CmdletEmitter.EmitNew(naming, new EmitContext("Test.Client"), "User", [], complex, []); + + Assert.Contains("public Test.Client.Models.PasswordProfile? PasswordProfile { get; set; }", source); + Assert.Contains("body.PasswordProfile = PasswordProfile;", source); + + Assert.Contains("public Test.Client.Models.AssignedLicense[]? AssignedLicenses { get; set; }", source); + Assert.Contains("body.AssignedLicenses = AssignedLicenses!.ToList();", source); + + Assert.Contains("public Test.Client.Models.RoleType?[]? Roles { get; set; }", source); + + // The removed hard-coded special case must not come back in any form. + Assert.DoesNotContain("ForceChangePasswordNextSignIn", source); + Assert.DoesNotContain("new PasswordProfile", source); + } + // PATCH-only resources (/places/{id}) have no GetAsync on their kiota builder, so the // 204 re-fetch must be emitted only when the path has a GET (found by compiling the // Calendar module). Without the re-fetch, a bodiless 204 writes nothing — same as the @@ -54,11 +86,11 @@ public void UpdateEmitsReFetchOnlyWhenPathHasGet() var props = new[] { new CmdletProperty("displayName", "DisplayName", "string", IsArray: false) }; var ctx = new EmitContext("Test.Client"); - var withGet = CmdletEmitter.EmitUpdate(naming, ctx, "Place", props, hasPasswordProfile: false, reFetchAfterUpdate: true); + var withGet = CmdletEmitter.EmitUpdate(naming, ctx, "Place", props, [], [], reFetchAfterUpdate: true); Assert.Contains("re-fetching the updated resource", withGet); Assert.Contains(".GetAsync()", withGet); - var withoutGet = CmdletEmitter.EmitUpdate(naming, ctx, "Place", props, hasPasswordProfile: false, reFetchAfterUpdate: false); + var withoutGet = CmdletEmitter.EmitUpdate(naming, ctx, "Place", props, [], [], reFetchAfterUpdate: false); Assert.DoesNotContain("re-fetching the updated resource", withoutGet); Assert.DoesNotContain(".GetAsync()", withoutGet); Assert.Contains("if (result is not null)", withoutGet); diff --git a/tools/WrapperGenerator.Tests/NamingTests.cs b/tools/WrapperGenerator.Tests/NamingTests.cs index 3e8c8ebca65..a065649b851 100644 --- a/tools/WrapperGenerator.Tests/NamingTests.cs +++ b/tools/WrapperGenerator.Tests/NamingTests.cs @@ -27,7 +27,7 @@ public sealed class SingularizerTests // "Whois" also hits the is-guard — a deliberate correction, not a parity pin: the SDK // ships Get-MgSecurityThreatIntelligenceHostWhoi (AutoRest inflected the trailing // "whois" segment) while its 28 whoisRecords/whoisHistoryRecords siblings keep "Whois". - // See edge-cases/naming-edge-cases.md. + // See docs/edge-cases/naming-edge-cases.md. [InlineData("Whois", "Whois")] // plain s [InlineData("Messages", "Message")] @@ -140,7 +140,7 @@ public void ResolvesPublishedSdkNames(string method, string path, string expecte [Theory] // Deliberate corrections: the published name is wrong (an AutoRest naming defect) and the // generator emits the corrected name instead of reproducing it. Every entry here must have - // an edge-cases/naming-edge-cases.md entry and a matching row in + // an docs/edge-cases/naming-edge-cases.md entry and a matching row in // Compare-WrapperCmdletNames.ps1's $deliberateCorrections table, so the parity gate // reports it as [CORRECTED], not a failure. // Shipped: Get-MgSecurityThreatIntelligenceHostWhoi — the only whois-family cmdlet (of 30) diff --git a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs index 9d704719812..5adc167c64d 100644 --- a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs +++ b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using Microsoft.OpenApi; using WrapperGenerator; using Xunit; @@ -8,26 +10,59 @@ namespace WrapperGenerator.Tests; public sealed class SchemaPropertiesTests { + private static OpenApiSchema Scalar(JsonSchemaType type, bool readOnly = false, string? format = null) => + new() { Type = type, ReadOnly = readOnly, Format = format }; + + // Component schemas the tests' $refs point at. Classification resolves a reference before + // deciding what it is, so the target's shape is what matters, not the reference itself. + private static readonly Dictionary Components = new(StringComparer.Ordinal) + { + ["graph.passwordProfile"] = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary { ["password"] = Scalar(JsonSchemaType.String) }, + }, + ["graph.assignedLicense"] = new OpenApiSchema { Type = JsonSchemaType.Object }, + ["graph.importance"] = new OpenApiSchema { Type = JsonSchemaType.String, Enum = [new System.Text.Json.Nodes.JsonArray()] }, + // Graph's marker for "this numeric may also arrive as INF/-INF/NaN". The VALUES are what + // identify the encoding, so they are real here rather than a placeholder. + ["graph.referenceNumeric"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Enum = [JsonValue.Create("-INF")!, JsonValue.Create("INF")!, JsonValue.Create("NaN")!], + }, + // A string enum that is NOT the sentinel set: a meaningful alternative, not an encoding. + ["graph.currency"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Enum = [JsonValue.Create("usd")!, JsonValue.Create("eur")!], + }, + }; + + private static IOpenApiSchema? Resolve(string id) => Components.TryGetValue(id, out var s) ? s : null; + + private static BodyProperties ClassifyBody(Dictionary properties, params string[] required) => + SchemaProperties.Classify( + new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = properties, + Required = new HashSet(required, StringComparer.Ordinal), + }, + Resolve); + // Kiota strips underscores when naming model members: signIn's "riskEventTypes_v2" // becomes RiskEventTypesV2 (verified against a generated SignIn model). The body // assignment targets that member, so extraction must produce the same name. [Fact] public void MapsUnderscorePropertyNamesTheWayKiotaDoes() { - var schema = new OpenApiSchema + var classified = ClassifyBody(new Dictionary { - Type = JsonSchemaType.Object, - Properties = new Dictionary - { - ["riskEventTypes_v2"] = new OpenApiSchema - { - Type = JsonSchemaType.Array, - Items = new OpenApiSchema { Type = JsonSchemaType.String }, - }, - }, - }; + ["riskEventTypes_v2"] = new OpenApiSchema { Type = JsonSchemaType.Array, Items = Scalar(JsonSchemaType.String) }, + }); - var property = Assert.Single(SchemaProperties.ExtractPrimitiveProperties(schema)); + var property = Assert.Single(classified.Scalars); Assert.Equal("RiskEventTypesV2", property.PascalName); Assert.Equal("riskEventTypes_v2", property.OpenApiName); } @@ -39,13 +74,13 @@ public void MapsUnderscorePropertyNamesTheWayKiotaDoes() [Fact] public void SuffixesBodyPropertyThatCollidesWithPathParameter() { - var properties = new[] + var scalars = new[] { new CmdletProperty("deviceId", "DeviceId", "string", IsArray: false), new CmdletProperty("displayName", "DisplayName", "string", IsArray: false), }; - var resolved = SchemaProperties.ResolveParameterNameCollisions(properties, new[] { "DeviceId" }); + var (resolved, _, _) = SchemaProperties.ResolveParameterNameCollisions(scalars, [], [], ["DeviceId"]); var renamed = Assert.Single(resolved, p => p.OpenApiName == "deviceId"); Assert.Equal("DeviceId1", renamed.ParameterName); @@ -55,121 +90,423 @@ public void SuffixesBodyPropertyThatCollidesWithPathParameter() Assert.Equal("DisplayName", untouched.ParameterName); } - private static OpenApiSchema Scalar(JsonSchemaType type, bool readOnly = false, string? format = null) => - new() { Type = type, ReadOnly = readOnly, Format = format }; + // Scalars and complex properties share one C# property namespace on the emitted class, so + // a complex property must not be handed a name a scalar (or a path id) already took. + [Fact] + public void ResolvesCollisionsAcrossScalarAndComplexProperties() + { + var scalars = new[] { new CmdletProperty("photo", "Photo", "string", IsArray: false) }; + var complex = new[] { new ComplexProperty("photo2", "Photo", "graph.passwordProfile", IsArray: false, IsEnum: false) }; + + var (resolvedScalars, resolvedComplex, _) = SchemaProperties.ResolveParameterNameCollisions(scalars, complex, [], []); + + Assert.Equal("Photo", resolvedScalars[0].ParameterName); + Assert.Equal("Photo1", resolvedComplex[0].ParameterName); + Assert.Equal("Photo", resolvedComplex[0].PascalName); + } [Fact] - public void KeepsPrimitivesAndPrimitiveArrays_ExcludesServerManagedAndComplex() + public void KeepsPrimitivesAndPrimitiveArrays_ExcludesServerManagedAndNavigation() { - var body = new OpenApiSchema + var classified = ClassifyBody(new Dictionary { - Type = JsonSchemaType.Object, - Properties = new Dictionary + // bound + ["displayName"] = Scalar(JsonSchemaType.String), + ["accountEnabled"] = Scalar(JsonSchemaType.Boolean), + ["businessPhones"] = new OpenApiSchema { Type = JsonSchemaType.Array, Items = Scalar(JsonSchemaType.String) }, + // excluded + ["id"] = Scalar(JsonSchemaType.String), // server-assigned + ["@odata.type"] = Scalar(JsonSchemaType.String), // OData control data + ["createdDateTime"] = Scalar(JsonSchemaType.String, readOnly: true), // ReadOnly + ["manager"] = new OpenApiSchema // relationship, not a body field { - // kept - ["displayName"] = Scalar(JsonSchemaType.String), - ["accountEnabled"] = Scalar(JsonSchemaType.Boolean), - ["jobTitle"] = Scalar(JsonSchemaType.Integer), - ["businessPhones"] = new OpenApiSchema { Type = JsonSchemaType.Array, Items = Scalar(JsonSchemaType.String) }, - // excluded - ["id"] = Scalar(JsonSchemaType.String), // server-assigned - ["@odata.type"] = Scalar(JsonSchemaType.String), // @-prefixed OData control - ["createdDateTime"] = Scalar(JsonSchemaType.String, readOnly: true), // ReadOnly - ["assignedLicenses"] = new OpenApiSchema // nested complex - { - Type = JsonSchemaType.Object, - Properties = new Dictionary { ["skuId"] = Scalar(JsonSchemaType.String) }, - }, + Type = JsonSchemaType.Object, + Extensions = new Dictionary { ["x-ms-navigationProperty"] = new JsonNodeExtension(true) }, }, - }; - - var names = SchemaProperties.ExtractPrimitiveProperties(body).Select(p => p.OpenApiName).ToHashSet(); + }); + var names = classified.Scalars.Select(p => p.OpenApiName).ToHashSet(); Assert.Contains("displayName", names); Assert.Contains("accountEnabled", names); - Assert.Contains("jobTitle", names); Assert.Contains("businessPhones", names); - Assert.DoesNotContain("id", names); - Assert.DoesNotContain("@odata.type", names); - Assert.DoesNotContain("createdDateTime", names); - Assert.DoesNotContain("assignedLicenses", names); + Assert.Equal(4, classified.Excluded.Count); + Assert.Empty(classified.Complex); + Assert.Empty(classified.Unsupported); } + // The property that motivated typed binding: Graph writes a nullable complex property as + // anyOf[$ref, {type: object, nullable: true}], and it must bind to the referenced model. [Fact] - public void MapsScalarAndArrayShapes() + public void BindsNullableReferenceComposition() { - var body = new OpenApiSchema + var classified = ClassifyBody(new Dictionary { - Type = JsonSchemaType.Object, - Properties = new Dictionary + ["passwordProfile"] = new OpenApiSchema { - ["displayName"] = Scalar(JsonSchemaType.String), - ["businessPhones"] = new OpenApiSchema { Type = JsonSchemaType.Array, Items = Scalar(JsonSchemaType.String) }, + AnyOf = + [ + new OpenApiSchemaReference("graph.passwordProfile"), + new OpenApiSchema { Type = JsonSchemaType.Object }, + ], }, - }; + }, required: "passwordProfile"); - var props = SchemaProperties.ExtractPrimitiveProperties(body); + var complex = Assert.Single(classified.Complex); + Assert.Equal("passwordProfile", complex.OpenApiName); + Assert.Equal("PasswordProfile", complex.PascalName); + Assert.Equal("graph.passwordProfile", complex.ReferenceId); + Assert.False(complex.IsArray); + } - var scalar = props.Single(p => p.OpenApiName == "displayName"); - Assert.False(scalar.IsArray); - Assert.Equal("string", scalar.PsTypeName); - Assert.Equal("DisplayName", scalar.PascalName); + [Fact] + public void BindsDirectReferenceAndReferenceArray() + { + var classified = ClassifyBody(new Dictionary + { + ["passwordProfile"] = new OpenApiSchemaReference("graph.passwordProfile"), + ["assignedLicenses"] = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Items = new OpenApiSchemaReference("graph.assignedLicense"), + }, + }); + + var single = Assert.Single(classified.Complex, p => p.OpenApiName == "passwordProfile"); + Assert.False(single.IsArray); - var array = props.Single(p => p.OpenApiName == "businessPhones"); + var array = Assert.Single(classified.Complex, p => p.OpenApiName == "assignedLicenses"); Assert.True(array.IsArray); - Assert.Equal("string[]", array.PsTypeName); - Assert.Equal("BusinessPhones", array.PascalName); + Assert.Equal("graph.assignedLicense", array.ReferenceId); } + // An enum reference binds like a model reference: kiota emits both as named types in the + // models namespace (Models/Importance.cs holds "public enum Importance"), so one path + // resolves both. PowerShell converts a string argument to the enum on binding. [Fact] - public void MapsNumericFormatsWithoutDataLoss() + public void BindsReferenceToEnumAsANamedType() { - var body = new OpenApiSchema + var classified = ClassifyBody(new Dictionary { - Type = JsonSchemaType.Object, - Properties = new Dictionary + ["importance"] = new OpenApiSchemaReference("graph.importance"), + }); + + Assert.Empty(classified.Unsupported); + var complex = Assert.Single(classified.Complex); + Assert.Equal("importance", complex.OpenApiName); + Assert.Equal("graph.importance", complex.ReferenceId); + } + + // Every mapping here was read off a generated Graph client; a wrong CLR name is a compile + // error in the module, so these are pinned rather than trusted to kiota's documentation. + [Theory] + [InlineData("date-time", "global::System.DateTimeOffset")] + [InlineData("uuid", "global::System.Guid")] + [InlineData("duration", "global::System.TimeSpan")] + [InlineData("date", "global::Microsoft.Kiota.Abstractions.Date")] + [InlineData("time", "global::Microsoft.Kiota.Abstractions.Time")] + [InlineData("base64url", "byte[]")] + [InlineData("binary", "byte[]")] + public void MapsFormattedStringsToTheTypeKiotaGenerates(string format, string expected) + { + var classified = ClassifyBody(new Dictionary + { + ["value"] = Scalar(JsonSchemaType.String, format: format), + }); + + Assert.Equal(expected, Assert.Single(classified.Scalars).PsTypeName); + } + + // An unrecognised format must be reported, never bound as plain string: kiota would have + // mapped it to some other CLR type and the assignment would not compile. + [Fact] + public void ReportsUnknownStringFormatRatherThanFallingBackToString() + { + var classified = ClassifyBody(new Dictionary + { + ["odd"] = Scalar(JsonSchemaType.String, format: "some-future-format"), + }); + + Assert.Empty(classified.Scalars); + Assert.Equal(UnsupportedShape.UnknownFormat, Assert.Single(classified.Unsupported).Shape); + } + + // uint8 generates as byte? (rgbColor.r/g/b). int16 has no short? anywhere in the generated + // clients, so kiota widens it to int and so must we. + [Theory] + [InlineData("uint8", JsonSchemaType.Integer, "byte")] + [InlineData("int16", JsonSchemaType.Integer, "int")] + public void MapsNarrowIntegerFormatsTheWayKiotaDoes(string format, JsonSchemaType type, string expected) + { + var classified = ClassifyBody(new Dictionary + { + ["n"] = Scalar(type, format: format), + }); + + Assert.Equal(expected, Assert.Single(classified.Scalars).PsTypeName); + } + + // Graph writes a numeric that may also carry OData's INF/NaN string as a three-way union. + // Kiota keeps the numeric (bookingService.price -> double?), so the numeric branch binds. + [Fact] + public void BindsTheNumericBranchOfGraphsInfinityUnion() + { + var classified = ClassifyBody(new Dictionary + { + ["price"] = new OpenApiSchema { - ["riskScore"] = Scalar(JsonSchemaType.Number), // fractions must survive - ["sizeInBytes"] = Scalar(JsonSchemaType.Integer, format: "int64"), // values > 2^31 must survive - ["retryCount"] = Scalar(JsonSchemaType.Integer, format: "int32"), - ["plainCount"] = Scalar(JsonSchemaType.Integer), - // Graph's docs declare Edm.Int32/Int64 as type "number" with the format carrying - // the real type (mailFolder.childFolderCount, messageRule.sequence). The format - // must win or the parameter type contradicts the Kiota model and won't compile. - ["childFolderCount"] = Scalar(JsonSchemaType.Number, format: "int32"), - ["quotaUsed"] = Scalar(JsonSchemaType.Number, format: "int64"), - ["confidence"] = Scalar(JsonSchemaType.Number, format: "float"), + OneOf = + [ + new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" }, + new OpenApiSchema { Type = JsonSchemaType.String }, + new OpenApiSchemaReference("graph.referenceNumeric"), + ], }, - }; + }); - var props = SchemaProperties.ExtractPrimitiveProperties(body); + Assert.Empty(classified.Unsupported); + Assert.Equal("double", Assert.Single(classified.Scalars).PsTypeName); + } - Assert.Equal("double", props.Single(p => p.OpenApiName == "riskScore").PsTypeName); - Assert.Equal("long", props.Single(p => p.OpenApiName == "sizeInBytes").PsTypeName); - Assert.Equal("int", props.Single(p => p.OpenApiName == "retryCount").PsTypeName); - Assert.Equal("int", props.Single(p => p.OpenApiName == "plainCount").PsTypeName); - Assert.Equal("int", props.Single(p => p.OpenApiName == "childFolderCount").PsTypeName); - Assert.Equal("long", props.Single(p => p.OpenApiName == "quotaUsed").PsTypeName); - Assert.Equal("float", props.Single(p => p.OpenApiName == "confidence").PsTypeName); + // Without the sentinel enum, "number or string" is an ordinary union whose string arm means + // something. Binding the numeric would silently discard it, so the sentinel is required + // evidence that the string arm is only OData's non-finite encoding. + [Fact] + public void ReportsNumericAndPlainStringUnionWithNoSentinelEnum() + { + var classified = ClassifyBody(new Dictionary + { + ["amount"] = new OpenApiSchema + { + OneOf = + [ + new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" }, + new OpenApiSchema { Type = JsonSchemaType.String }, + ], + }, + }); + + Assert.Empty(classified.Scalars); + Assert.Equal(UnsupportedShape.Union, Assert.Single(classified.Unsupported).Shape); } + // A referenced string enum that is not the sentinel set is a real alternative too. [Fact] - public void HasPasswordProfile_DetectsDirectAndViaAllOf() + public void ReportsNumericUnionWhoseEnumIsNotTheSentinelSet() { - var withProfile = new OpenApiSchema + var classified = ClassifyBody(new Dictionary { - Properties = new Dictionary { ["passwordProfile"] = new OpenApiSchema { Type = JsonSchemaType.Object } }, + ["price"] = new OpenApiSchema + { + OneOf = + [ + new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" }, + new OpenApiSchemaReference("graph.currency"), + ], + }, + }); + + Assert.Empty(classified.Scalars); + Assert.Equal(UnsupportedShape.Union, Assert.Single(classified.Unsupported).Shape); + } + + // A numeric beside a MODEL is a real choice, not the INF encoding. Recognising only + // "exactly one numeric branch" would bind the numeric here and silently discard the model + // arm, so the whole structure has to match. + [Fact] + public void ReportsUnionOfANumericAndAModel() + { + var classified = ClassifyBody(new Dictionary + { + ["either"] = new OpenApiSchema + { + OneOf = + [ + new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" }, + new OpenApiSchemaReference("graph.passwordProfile"), + ], + }, + }); + + Assert.Empty(classified.Scalars); + Assert.Equal(UnsupportedShape.Union, Assert.Single(classified.Unsupported).Shape); + } + + // A numeric beside a formatted string is likewise not the INF encoding: the string arm + // carries its own CLR type rather than being a stringish alternative. + [Fact] + public void ReportsUnionOfANumericAndAFormattedString() + { + var classified = ClassifyBody(new Dictionary + { + ["either"] = new OpenApiSchema + { + OneOf = + [ + new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" }, + new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" }, + ], + }, + }); + + Assert.Empty(classified.Scalars); + Assert.Equal(UnsupportedShape.Union, Assert.Single(classified.Unsupported).Shape); + } + + // Two numeric branches is a real choice, not the INF encoding: binding one would silently + // pick a type for the caller. + [Fact] + public void ReportsUnionWithMoreThanOneNumericBranch() + { + var classified = ClassifyBody(new Dictionary + { + ["ambiguous"] = new OpenApiSchema + { + OneOf = + [ + new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" }, + new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" }, + ], + }, + }); + + Assert.Empty(classified.Scalars); + Assert.Equal(UnsupportedShape.Union, Assert.Single(classified.Unsupported).Shape); + } + + // Only a single reference plus pure nullability unwraps. A choice between two real + // schemas is a union: picking one arm silently would bind the caller to the wrong type. + [Fact] + public void ReportsGenuineUnionRatherThanChoosingABranch() + { + var classified = ClassifyBody(new Dictionary + { + ["either"] = new OpenApiSchema + { + AnyOf = + [ + new OpenApiSchemaReference("graph.passwordProfile"), + new OpenApiSchemaReference("graph.assignedLicense"), + ], + }, + }); + + Assert.Empty(classified.Complex); + Assert.Equal(UnsupportedShape.Union, Assert.Single(classified.Unsupported).Shape); + } + + // An anonymous object still has no name kiota would agree with, so it stays reported even + // though a formatted scalar beside it now binds. + [Fact] + public void ReportsInlineObjectWhileBindingAFormattedScalarBesideIt() + { + var classified = ClassifyBody(new Dictionary + { + ["anonymous"] = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary { ["x"] = Scalar(JsonSchemaType.String) }, + }, + ["birthday"] = Scalar(JsonSchemaType.String, format: "date-time"), + }); + + Assert.Equal(UnsupportedShape.InlineObject, Assert.Single(classified.Unsupported).Shape); + Assert.Equal("global::System.DateTimeOffset", Assert.Single(classified.Scalars, p => p.OpenApiName == "birthday").PsTypeName); + } + + // Every property seen lands in exactly one bucket. The coverage sweep relies on this + // identity holding, so a shape that silently falls through would be caught here. + [Fact] + public void EveryPropertyIsAccountedForExactlyOnce() + { + var properties = new Dictionary + { + ["displayName"] = Scalar(JsonSchemaType.String), + ["businessPhones"] = new OpenApiSchema { Type = JsonSchemaType.Array, Items = Scalar(JsonSchemaType.String) }, + ["passwordProfile"] = new OpenApiSchemaReference("graph.passwordProfile"), + ["importance"] = new OpenApiSchemaReference("graph.importance"), + ["birthday"] = Scalar(JsonSchemaType.String, format: "date-time"), + ["id"] = Scalar(JsonSchemaType.String), }; - Assert.True(SchemaProperties.HasPasswordProfile(withProfile)); - var viaAllOf = new OpenApiSchema { AllOf = new List { withProfile } }; - Assert.True(SchemaProperties.HasPasswordProfile(viaAllOf)); + var classified = ClassifyBody(properties); - var without = new OpenApiSchema + Assert.Equal( + properties.Count, + classified.Scalars.Count + classified.Complex.Count + classified.Unsupported.Count + classified.Excluded.Count); + } + + // PropertiesSeen is counted independently of the buckets, so the reported total cannot be + // a restatement of their sum: every property reached is either routed or the classifier + // throws. Asserting it here keeps the runtime reconciliation line meaningful. + [Fact] + public void ReportsIndependentlyCountedTotalThatMatchesTheBuckets() + { + var properties = new Dictionary { - Properties = new Dictionary { ["displayName"] = Scalar(JsonSchemaType.String) }, + ["displayName"] = Scalar(JsonSchemaType.String), + ["passwordProfile"] = new OpenApiSchemaReference("graph.passwordProfile"), + ["importance"] = new OpenApiSchemaReference("graph.importance"), + ["id"] = Scalar(JsonSchemaType.String), }; - Assert.False(SchemaProperties.HasPasswordProfile(without)); + + var classified = ClassifyBody(properties); + + Assert.Equal(properties.Count, classified.PropertiesSeen); + Assert.Equal( + classified.PropertiesSeen, + classified.Scalars.Count + classified.Complex.Count + classified.Unsupported.Count + classified.Excluded.Count); + } + + // A property inherited through allOf and also restated on the child is one property, not + // two: the dedupe must be reflected in the independent count as well, or the invariant + // would fire spuriously on a perfectly normal Graph schema. + [Fact] + public void CountsAPropertyOnceWhenAllOfRestatesIt() + { + var classified = SchemaProperties.Classify( + new OpenApiSchema + { + Type = JsonSchemaType.Object, + AllOf = + [ + new OpenApiSchema + { + Properties = new Dictionary { ["displayName"] = Scalar(JsonSchemaType.String) }, + }, + ], + Properties = new Dictionary { ["displayName"] = Scalar(JsonSchemaType.String) }, + }, + Resolve); + + Assert.Equal(1, classified.PropertiesSeen); + Assert.Single(classified.Scalars); + } + + [Fact] + public void MapsNumericFormatsWithoutDataLoss() + { + var classified = ClassifyBody(new Dictionary + { + ["riskScore"] = Scalar(JsonSchemaType.Number), // fractions must survive + ["sizeInBytes"] = Scalar(JsonSchemaType.Integer, format: "int64"), // values > 2^31 must survive + ["retryCount"] = Scalar(JsonSchemaType.Integer, format: "int32"), + ["plainCount"] = Scalar(JsonSchemaType.Integer), + // Graph's docs declare Edm.Int32/Int64 as type "number" with the format carrying + // the real type (mailFolder.childFolderCount, messageRule.sequence). The format + // must win or the parameter type contradicts the Kiota model and won't compile. + ["childFolderCount"] = Scalar(JsonSchemaType.Number, format: "int32"), + ["quotaUsed"] = Scalar(JsonSchemaType.Number, format: "int64"), + ["confidence"] = Scalar(JsonSchemaType.Number, format: "float"), + }); + + var props = classified.Scalars; + Assert.Equal("double", props.Single(p => p.OpenApiName == "riskScore").PsTypeName); + Assert.Equal("long", props.Single(p => p.OpenApiName == "sizeInBytes").PsTypeName); + Assert.Equal("int", props.Single(p => p.OpenApiName == "retryCount").PsTypeName); + Assert.Equal("int", props.Single(p => p.OpenApiName == "plainCount").PsTypeName); + Assert.Equal("int", props.Single(p => p.OpenApiName == "childFolderCount").PsTypeName); + Assert.Equal("long", props.Single(p => p.OpenApiName == "quotaUsed").PsTypeName); + Assert.Equal("float", props.Single(p => p.OpenApiName == "confidence").PsTypeName); } } diff --git a/tools/WrapperGenerator.Tests/SpecShapeTests.cs b/tools/WrapperGenerator.Tests/SpecShapeTests.cs new file mode 100644 index 00000000000..70bf09758fa --- /dev/null +++ b/tools/WrapperGenerator.Tests/SpecShapeTests.cs @@ -0,0 +1,142 @@ +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Reader; +using Xunit; + +namespace WrapperGenerator.Tests; + +// Pins the two spec facts complex-property binding depends on. Both are properties of the +// Graph documents AND of the reader that parses them, so an in-memory OpenApiSchema cannot +// prove either - these parse real YAML. +// +// If either breaks (a reader upgrade drops unknown extensions, or Graph changes how it marks +// navigation properties), binding would start emitting parameters for navigation properties +// like -AdhocCalls or -AppRoleAssignments, which are not request-body fields at all. That +// failure would be silent in the generator and only visible as nonsense cmdlet surface, so it +// is gated here instead. +public sealed class SpecShapeTests +{ + // The reader models a $ref as an OpenApiSchemaReference rather than an inlined schema; + // read it through the public API here so these tests pin the reader, not our helper. + private static string? ReferenceIdOf(IOpenApiSchema schema) => + schema is OpenApiSchemaReference reference ? reference.Reference?.Id : null; + + private static OpenApiDocument Parse(string yaml) + { + var settings = new OpenApiReaderSettings(); + settings.AddYamlReader(); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(yaml)); + var result = OpenApiDocument.LoadAsync(stream, settings: settings, cancellationToken: CancellationToken.None) + .GetAwaiter().GetResult(); + return result.Document!; + } + + // Graph marks navigation properties with x-ms-navigationProperty: true and does NOT set + // readOnly on them, so the extension is the only signal that keeps them out of the bound + // parameter set. + [Fact] + public void ReaderPreservesNavigationPropertyExtension() + { + const string yaml = """ + openapi: 3.0.1 + info: { title: probe, version: 1.0.0 } + paths: {} + components: + schemas: + probe.entity: + type: object + properties: + displayName: + type: string + adhocCalls: + type: array + items: + $ref: '#/components/schemas/probe.child' + x-ms-navigationProperty: true + probe.child: + type: object + properties: + id: { type: string } + """; + + var entity = Parse(yaml).Components!.Schemas!["probe.entity"]; + var nav = entity.Properties!["adhocCalls"]; + var structural = entity.Properties!["displayName"]; + + Assert.True(nav.Extensions is not null && nav.Extensions.ContainsKey("x-ms-navigationProperty"), + "Reader dropped x-ms-navigationProperty; navigation properties can no longer be excluded from body binding."); + Assert.False(nav.ReadOnly, "Graph does not set readOnly on navigation properties - the extension is the only signal."); + Assert.False(structural.Extensions?.ContainsKey("x-ms-navigationProperty") ?? false); + } + + // A nullable complex property is expressed as anyOf[ $ref, { type: object, nullable: true } ] + // - verified against user.passwordProfile, the property that motivated typed binding. The + // branch carrying the $ref must stay resolvable through the reader. + [Fact] + public void ReaderPreservesNullableRefCompositionShape() + { + const string yaml = """ + openapi: 3.0.1 + info: { title: probe, version: 1.0.0 } + paths: {} + components: + schemas: + probe.user: + type: object + properties: + passwordProfile: + anyOf: + - $ref: '#/components/schemas/probe.passwordProfile' + - type: object + nullable: true + probe.passwordProfile: + type: object + properties: + password: { type: string } + """; + + var property = Parse(yaml).Components!.Schemas!["probe.user"].Properties!["passwordProfile"]; + + Assert.NotNull(property.AnyOf); + Assert.Equal(2, property.AnyOf!.Count); + var refs = property.AnyOf.Where(b => ReferenceIdOf(b) is not null).ToList(); + Assert.Single(refs); + Assert.Equal("probe.passwordProfile", ReferenceIdOf(refs[0])); + } + + // A $ref does not imply an object: microsoft.graph.importance is a string enum reached the + // same way passwordProfile is. Classification has to resolve the reference and look at the + // target, or enums would be bound as model-typed parameters that do not compile. + [Fact] + public void ReferencedSchemaMayBeAnEnumNotAnObject() + { + const string yaml = """ + openapi: 3.0.1 + info: { title: probe, version: 1.0.0 } + paths: {} + components: + schemas: + probe.message: + type: object + properties: + importance: + $ref: '#/components/schemas/probe.importance' + probe.importance: + type: string + enum: [low, normal, high] + """; + + var document = Parse(yaml); + var importance = document.Components!.Schemas!["probe.message"].Properties!["importance"]; + + var referenceId = ReferenceIdOf(importance); + Assert.Equal("probe.importance", referenceId); + + var target = document.Components.Schemas[referenceId!]; + Assert.True((target.Type & ~JsonSchemaType.Null) == JsonSchemaType.String); + Assert.NotEmpty(target.Enum!); + } +} diff --git a/tools/WrapperGenerator/CmdletEmitter.cs b/tools/WrapperGenerator/CmdletEmitter.cs index 985309edfe6..21b90c41cc1 100644 --- a/tools/WrapperGenerator/CmdletEmitter.cs +++ b/tools/WrapperGenerator/CmdletEmitter.cs @@ -200,7 +200,7 @@ protected override void ProcessRecord() { {{AuthBlock}} - {{entityType}} result; + {{entityType}}? result; try { result = client.{{naming.BuilderExpression}}.GetAsync(requestConfiguration => @@ -295,7 +295,7 @@ protected override void ProcessRecord() { {{AuthBlock}} - {{collectionResponseType}} result; + {{collectionResponseType}}? result; try { result = client.{{naming.BuilderExpression}}.GetAsync(requestConfiguration => @@ -307,7 +307,10 @@ protected override void ProcessRecord() } {{CatchBlock(TargetId(naming))}} - WriteObject(result.Value, enumerateCollection: true); + // A collection response and its Value are both nullable on the kiota client; an + // empty page writes nothing rather than dereferencing null. + if (result?.Value is { } items) + WriteObject(items, enumerateCollection: true); } } } @@ -450,11 +453,13 @@ protected override void ProcessRecord() """; } - public static string EmitNew(CmdletNaming naming, EmitContext ctx, string entityType, IReadOnlyList properties, bool hasPasswordProfile) + public static string EmitNew(CmdletNaming naming, EmitContext ctx, string entityType, IReadOnlyList properties, IReadOnlyList complexProperties, IReadOnlyList untypedProperties) { ArgumentNullException.ThrowIfNull(naming); ArgumentNullException.ThrowIfNull(ctx); ArgumentNullException.ThrowIfNull(properties); + ArgumentNullException.ThrowIfNull(complexProperties); + ArgumentNullException.ThrowIfNull(untypedProperties); return $$""" #nullable enable @@ -476,7 +481,8 @@ public class {{naming.ClassName}} : PSCmdlet { {{PathParams(naming)}} {{EmitPropertyParameters(properties)}} -{{(hasPasswordProfile ? EmitPasswordProfileParameters() : "")}} +{{EmitComplexParameters(complexProperties)}} +{{EmitUntypedParameters(untypedProperties)}} {{HeaderParamDecls(naming)}} {{GenericHeadersParamDecl()}} @@ -489,7 +495,8 @@ protected override void ProcessRecord() var body = new {{entityType}}(); {{EmitPropertyAssignments(properties)}} -{{(hasPasswordProfile ? EmitPasswordProfileAssignment() : "")}} +{{EmitComplexAssignments(complexProperties)}} +{{EmitUntypedAssignments(untypedProperties)}} {{AuthBlock}} {{entityType}}? result; @@ -507,11 +514,13 @@ protected override void ProcessRecord() """; } - public static string EmitUpdate(CmdletNaming naming, EmitContext ctx, string entityType, IReadOnlyList properties, bool hasPasswordProfile, bool reFetchAfterUpdate = true) + public static string EmitUpdate(CmdletNaming naming, EmitContext ctx, string entityType, IReadOnlyList properties, IReadOnlyList complexProperties, IReadOnlyList untypedProperties, bool reFetchAfterUpdate = true) { ArgumentNullException.ThrowIfNull(naming); ArgumentNullException.ThrowIfNull(ctx); ArgumentNullException.ThrowIfNull(properties); + ArgumentNullException.ThrowIfNull(complexProperties); + ArgumentNullException.ThrowIfNull(untypedProperties); return $$""" #nullable enable @@ -533,7 +542,8 @@ public class {{naming.ClassName}} : PSCmdlet { {{PathParams(naming)}} {{EmitPropertyParameters(properties)}} -{{(hasPasswordProfile ? EmitPasswordProfileParameters() : "")}} +{{EmitComplexParameters(complexProperties)}} +{{EmitUntypedParameters(untypedProperties)}} {{HeaderParamDecls(naming)}} {{GenericHeadersParamDecl()}} @@ -546,7 +556,8 @@ protected override void ProcessRecord() var body = new {{entityType}}(); {{EmitPropertyAssignments(properties)}} -{{(hasPasswordProfile ? EmitPasswordProfileAssignment() : "")}} +{{EmitComplexAssignments(complexProperties)}} +{{EmitUntypedAssignments(untypedProperties)}} {{AuthBlock}} {{entityType}}? result; @@ -621,12 +632,14 @@ public static string EmitSharedAuth(EmitContext ctx) #nullable enable using System; +using System.Collections; using System.Collections.Generic; using System.Management.Automation; using System.Threading; using System.Threading.Tasks; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; +using Microsoft.Kiota.Abstractions.Serialization; namespace {{ctx.CmdletNamespace}} { @@ -655,6 +668,80 @@ public Task AuthenticateRequestAsync(RequestInformation request, Dictionary(StringComparer.Ordinal); + foreach (DictionaryEntry entry in dictionary) + { + var key = entry.Key?.ToString(); + if (key is null) + continue; + // Dropped, not sent. The published SDK has no untyped bag to copy + // here, so this extends its top-level rule rather than inheriting it. + var member = From(entry.Value); + if (member is not null) + members[key] = member; + } + return members.Count == 0 ? null : new UntypedObject(members); + } + case IEnumerable sequence: + { + var items = new List(); + foreach (var item in sequence) + { + var node = From(item); + if (node is not null) + items.Add(node); + } + return items.Count == 0 ? null : new UntypedArray(items); + } + default: + // Stringifying an unrecognised type would send a value the caller never + // wrote; failing names the type so the gap is fixable. + throw new ArgumentException( + $"Cannot convert a value of type '{value.GetType().FullName}' to an untyped Graph value. Supported: string, boolean, number, hashtable, array."); + } + } + } } """; } @@ -693,25 +780,43 @@ private static string EmitPropertyAssignments(IReadOnlyList prop body.{{p.PascalName}} = {{(p.IsArray ? $"{p.ParameterName}!.ToList()" : p.ParameterName)}}; """)); - private static string EmitPasswordProfileParameters() => """ + // A complex property binds as its kiota model type. PowerShell converts a hashtable to that + // type on binding (the models have a parameterless constructor and settable properties), so + // the caller writes -PasswordProfile @{ Password = '...' } without constructing the type. + // TypeName is fully qualified: the models namespace is imported, but a model whose name + // matches a cmdlet parameter or BCL type would otherwise bind to the wrong symbol. + private static string EmitComplexParameters(IReadOnlyList properties) => + string.Join("\n", properties.Select(p => $$""" - [Parameter(Mandatory = false, - HelpMessage = "Required by Graph to create a user. Ignored if the resource has no passwordProfile.")] - public string? Password { get; set; } + [Parameter(Mandatory = false)] + public {{p.ElementNullableTypeName}}? {{p.ParameterName}} { get; set; } + """)); + + private static string EmitComplexAssignments(IReadOnlyList properties) => + string.Join("\n", properties.Select(p => $$""" + + if (this.IsParameterBound(nameof({{p.ParameterName}}))) + body.{{p.PascalName}} = {{(p.IsArray ? $"{p.ParameterName}!.ToList()" : p.ParameterName)}}; + """)); + + // A schema-less property takes object and converts, so the caller can pass an ordinary + // PowerShell value. A conversion result of null means "omit", which is why the assignment + // is guarded on the converted value and not merely on the parameter being bound. + private static string EmitUntypedParameters(IReadOnlyList properties) => + string.Join("\n", properties.Select(p => $$""" [Parameter(Mandatory = false)] - public bool? ForceChangePasswordNextSignIn { get; set; } - """; + public object? {{p.ParameterName}} { get; set; } + """)); - private static string EmitPasswordProfileAssignment() => """ + private static string EmitUntypedAssignments(IReadOnlyList properties) => + string.Join("\n", properties.Select(p => $$""" - if (this.IsParameterBound(nameof(Password)) || this.IsParameterBound(nameof(ForceChangePasswordNextSignIn))) + if (this.IsParameterBound(nameof({{p.ParameterName}}))) { - body.PasswordProfile = new PasswordProfile - { - Password = Password, - ForceChangePasswordNextSignIn = ForceChangePasswordNextSignIn ?? true, - }; + var {{p.LocalName}} = UntypedValue.From({{p.ParameterName}}); + if ({{p.LocalName}} is not null) + body.{{p.PascalName}} = {{p.LocalName}}; } - """; + """)); } diff --git a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs index 82974e0db3e..46690e50085 100644 --- a/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs +++ b/tools/WrapperGenerator/PowerShellWrapperGenerationService.cs @@ -28,6 +28,16 @@ public sealed partial class PowerShellWrapperGenerationService private readonly List fileCollisions = []; private readonly Dictionary kiotaReservedRenames; + // Body-property classification totals for this run; reported as one reconciliation line. + // propertiesSeenCount is accumulated from the classifier's own independent count so the + // reported total is not merely the sum of the buckets beside it. + private int propertiesSeenCount; + private int boundScalarCount; + private int boundComplexCount; + private int boundUntypedCount; + private int unsupportedPropertyCount; + private int excludedPropertyCount; + public PowerShellWrapperGenerationService(OpenApiDocument document, GeneratorConfig configuration, ILogger logger) { ArgumentNullException.ThrowIfNull(document); @@ -208,7 +218,7 @@ public async Task GenerateAsync(CancellationToken cancellationToken) written += await EmitGetOperationsAsync(getOperations, ctx, cancellationToken).ConfigureAwait(false); // All collisions for the run are reported together so one generation surfaces the - // complete list; see edge-cases/naming-edge-cases.md for how each kind is resolved. + // complete list; see docs/edge-cases/naming-edge-cases.md for how each kind is resolved. if (fileCollisions.Count > 0) { throw new InvalidOperationException( @@ -216,6 +226,10 @@ public async Task GenerateAsync(CancellationToken cancellationToken) $"Resolve each with a NamingOverrides rename or suppression.\n " + string.Join("\n ", fileCollisions)); } + LogBodyPropertyReconciliation( + propertiesSeenCount, + boundScalarCount, boundComplexCount, boundUntypedCount, excludedPropertyCount, unsupportedPropertyCount); + LogWroteFiles(written + 1, config.OutputPath); } @@ -340,6 +354,14 @@ private async Task WriteCmdletFileAsync(CmdletNaming naming, string source, private partial void LogSuppressedOperation(string method, string pathTemplate); [LoggerMessage(Level = LogLevel.Warning, Message = "Skipped {Method} {PathTemplate}: {Reason}")] private partial void LogSkippedUnsupportedOperation(string method, string pathTemplate, string reason); + // Information, not Warning: an unbindable body property is a known coverage gap per shape, + // not a defect in this run, and at Graph scale these would drown the operation warnings. + [LoggerMessage(Level = LogLevel.Information, Message = "Unbound body property {Noun}.{Property}: {Shape} (required={IsRequired})")] + private partial void LogSkippedBodyProperty(string noun, string property, string shape, bool isRequired); + [LoggerMessage(Level = LogLevel.Information, Message = "Excluded body property {Noun}.{Property}: {Policy}")] + private partial void LogExcludedBodyProperty(string noun, string property, string policy); + [LoggerMessage(Level = LogLevel.Information, Message = "Body properties classified={Classified} = scalar={Scalars} + model={Complex} + untyped={Untyped} + excluded={Excluded} + unsupported={Unsupported}")] + private partial void LogBodyPropertyReconciliation(int classified, int scalars, int complex, int untyped, int excluded, int unsupported); // True when the path contains a segment shape the emitters cannot handle yet: an OData // $-segment ($count/$value/$ref) or a parameterized function/action call (any segment @@ -387,9 +409,8 @@ private bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueSchema, return null; if (!TryResolveEntityTypeName(bodySchema, ctx.ModelsNamespace, out var entityType)) return null; - var properties = SchemaProperties.ResolveParameterNameCollisions( - SchemaProperties.ExtractPrimitiveProperties(bodySchema), naming.PathParamNames); - return CmdletEmitter.EmitNew(naming, ctx, entityType, properties, SchemaProperties.HasPasswordProfile(bodySchema)); + var (properties, complex, untyped) = BindBodyProperties(bodySchema, ctx, naming, entityType); + return CmdletEmitter.EmitNew(naming, ctx, entityType, properties, complex, untyped); } private string? EmitUpdateFor(CmdletNaming naming, EmitContext ctx, OpenApiOperation operation, bool canReFetch) @@ -400,11 +421,66 @@ private bool TryResolveListEntityTypeName(IOpenApiSchema collectionValueSchema, return null; if (!TryResolveEntityTypeName(bodySchema, ctx.ModelsNamespace, out var entityType)) return null; - var properties = SchemaProperties.ResolveParameterNameCollisions( - SchemaProperties.ExtractPrimitiveProperties(bodySchema), naming.PathParamNames); - return CmdletEmitter.EmitUpdate(naming, ctx, entityType, properties, SchemaProperties.HasPasswordProfile(bodySchema), canReFetch); + var (properties, complex, untyped) = BindBodyProperties(bodySchema, ctx, naming, entityType); + return CmdletEmitter.EmitUpdate(naming, ctx, entityType, properties, complex, untyped, canReFetch); + } + + // Classifies a request body and resolves each complex property's component-schema key to + // the kiota CLR type name, reusing ResolveModelTypeName so reserved-name renames and + // sub-namespace moves are applied in exactly one place. A property whose reference does not + // resolve is dropped with a diagnostic rather than emitted against a guessed type name, + // which would fail the module compile. + private (IReadOnlyList Scalars, IReadOnlyList Complex, IReadOnlyList Untyped) BindBodyProperties( + IOpenApiSchema bodySchema, EmitContext ctx, CmdletNaming naming, string entityType) + { + var classified = SchemaProperties.Classify(bodySchema, ResolveComponentSchema); + var (scalars, complex, untyped) = SchemaProperties.ResolveParameterNameCollisions( + classified.Scalars, classified.Complex, classified.Untyped, naming.PathParamNames); + + // C# forbids a member sharing its enclosing type's name, so kiota suffixes such a + // property with "Prop": microsoft.graph.list's own "list" property generates as + // List.ListProp (verified in a generated Files client). The assignment target has to + // match the member kiota emitted, or the module does not compile. + var enclosingTypeName = entityType[(entityType.LastIndexOf('.') + 1)..]; + scalars = [.. scalars.Select(p => p.PascalName == enclosingTypeName ? p with { PascalName = p.PascalName + "Prop" } : p)]; + complex = [.. complex.Select(p => p.PascalName == enclosingTypeName ? p with { PascalName = p.PascalName + "Prop" } : p)]; + untyped = [.. untyped.Select(p => p.PascalName == enclosingTypeName ? p with { PascalName = p.PascalName + "Prop" } : p)]; + + foreach (var skipped in classified.Unsupported) + LogSkippedBodyProperty(naming.Noun, skipped.OpenApiName, skipped.Shape.ToString(), skipped.IsRequired); + + // Named so an external reconciliation can tell a policy exclusion from an omission + // without re-deriving the policy from the spec. + foreach (var dropped in classified.Excluded) + LogExcludedBodyProperty(naming.Noun, dropped.OpenApiName, dropped.Policy.ToString()); + + // Totals for the run's reconciliation line: every classified property must end up in + // exactly one of these buckets, so a shape that silently fell through the classifier + // would show up as a mismatch at Graph scale, not just in the unit test. + propertiesSeenCount += classified.PropertiesSeen; + boundScalarCount += classified.Scalars.Count; + boundComplexCount += classified.Complex.Count; + boundUntypedCount += classified.Untyped.Count; + unsupportedPropertyCount += classified.Unsupported.Count; + excludedPropertyCount += classified.Excluded.Count; + + var parameters = new List(complex.Count); + foreach (var property in complex) + { + parameters.Add(new ComplexParameter( + property.PascalName, + property.ParameterName, + ResolveModelTypeName(property.ReferenceId, ctx.ModelsNamespace, modelSubNamespaces, kiotaReservedRenames), + property.IsArray, + property.IsEnum)); + } + var untypedParameters = untyped.Select(p => new UntypedParameter(p.PascalName, p.ParameterName)).ToList(); + return (scalars, parameters, untypedParameters); } + private IOpenApiSchema? ResolveComponentSchema(string referenceId) => + document.Components?.Schemas?.TryGetValue(referenceId, out var schema) == true ? schema : null; + private static bool HasNonJsonSuccessContent(OpenApiOperation operation) { if (operation.Responses is null) diff --git a/tools/WrapperGenerator/Program.cs b/tools/WrapperGenerator/Program.cs index 15d43b5dffa..19c588fd586 100644 --- a/tools/WrapperGenerator/Program.cs +++ b/tools/WrapperGenerator/Program.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.OpenApi; using Microsoft.OpenApi.Reader; @@ -22,6 +23,7 @@ private static async Task Main(string[] args) string? clientNamespace = null; var apiVersion = "v1.0"; var useCollisionData = true; + var logLevel = LogLevel.Warning; var includePaths = new List(); for (var i = 0; i < args.Length; i++) @@ -40,6 +42,13 @@ private static async Task Main(string[] args) case "--api-version": apiVersion = ArgValue(args, ref i); break; + case "--log-level": + if (!Enum.TryParse(ArgValue(args, ref i), ignoreCase: true, out logLevel)) + { + Console.Error.WriteLine("--log-level expects one of: Trace, Debug, Information, Warning, Error, Critical, None"); + return 2; + } + break; case "--no-collision-data": // Derivation mode: tools/Derive-CollisionResolutions.ps1 needs the raw // collision inventory, so the derived resolutions must not mask it. @@ -62,7 +71,7 @@ private static async Task Main(string[] args) if (specPath is null || outputPath is null || clientNamespace is null) { Console.Error.WriteLine( - "Usage: WrapperGenerator -d -o -n [--api-version v1.0|beta] [--no-collision-data] [--include-path '#GET,POST' ...]"); + "Usage: WrapperGenerator -d -o -n [--api-version v1.0|beta] [--no-collision-data] [--log-level Information] [--include-path '#GET,POST' ...]"); return 2; } @@ -78,7 +87,7 @@ private static async Task Main(string[] args) var config = new GeneratorConfig(ClientNamespaceName: clientNamespace, OutputPath: outputPath, ApiVersion: apiVersion, UseCollisionData: useCollisionData); - var service = new PowerShellWrapperGenerationService(document, config, new StderrLogger()); + var service = new PowerShellWrapperGenerationService(document, config, new StderrLogger(logLevel)); await service.GenerateAsync(CancellationToken.None).ConfigureAwait(false); // The generation service writes only *.g.cs. Also write a minimal kiota-lock.json recording diff --git a/tools/WrapperGenerator/README.md b/tools/WrapperGenerator/README.md index 60e4aa796a7..d8e5b15f7eb 100644 --- a/tools/WrapperGenerator/README.md +++ b/tools/WrapperGenerator/README.md @@ -6,7 +6,7 @@ Generates the PowerShell **cmdlets** for the Microsoft Graph SDK from Graph's Op The Microsoft Graph PowerShell SDK is thousands of cmdlets, and customers have scripts that depend on their exact names — `Get-MgUserMessage`, not `Get-MgUsersMessages`. Those names follow conventions, but the conventions are fiddly (singular nouns, a `Mg` prefix, a handful of hand-tuned exceptions), and the SDK's current generator (AutoRest) has quietly dropped cmdlets when names collided. -This tool regenerates those cmdlets from the same OpenAPI description **while reproducing the published names exactly**, so a regenerated module is a drop-in replacement. Because name parity is the hard part, most of the tool is a naming engine; the rest is a straightforward C# code emitter. The one exception to "exactly": a handful of published names are AutoRest naming defects (e.g. `Get-MgSecurityThreatIntelligenceHostWhoi`, where "Whois" lost its `s`) that the generator deliberately corrects — each is pinned by a test, allowlisted in the parity gate, and documented in the edge-case catalog ([edge-cases/naming-edge-cases.md](edge-cases/naming-edge-cases.md), one file per class of issue). +This tool regenerates those cmdlets from the same OpenAPI description **while reproducing the published names exactly**, so a regenerated module is a drop-in replacement. Because name parity is the hard part, most of the tool is a naming engine; the rest is a straightforward C# code emitter. The one exception to "exactly": a handful of published names are AutoRest naming defects (e.g. `Get-MgSecurityThreatIntelligenceHostWhoi`, where "Whois" lost its `s`) that the generator deliberately corrects — each is pinned by a test, allowlisted in the parity gate, and documented in the edge-case catalog ([docs/edge-cases/naming-edge-cases.md](docs/edge-cases/naming-edge-cases.md), one file per class of issue). ## What it produces @@ -54,7 +54,7 @@ Singularization runs per camel-case word (so `termsAndConditions` → `TermAndCo A few published names aren't algorithmic, and the spec publishes some routes the SDK never shipped. Both live as data in `NamingOverrides.cs` — renames mirroring the SDK's hand-written AutoRest directives, and suppressions for routes that ship nothing — each entry citing its evidence: the directive when one exists, otherwise the shipped-command inventory. Examples: the `GET /users/{id}/calendar` rename to `…UserDefaultCalendar` (Calendar.md), the `Solution` prefix strip under `/solutions/*` with the BackupRestore exception (Bookings.md), and the self-referential `sites/{id}/sites` rename to `SubSite`/`GroupSubSite` (Sites.md) — without which the sub-sites cmdlets would collide with `Get-MgSite` itself. The generator fails loudly on any such file collision rather than silently overwriting. -On top of the curated entries sits a **derived** layer: `tools/Derive-CollisionResolutions.ps1` replays every route from the checked-in collision inventory (`data/collision-inventory.v1.0.txt`, captured with `--no-collision-data`) against the shipped-command inventory and emits `data/collision-suppressions.v1.0.json` and `data/collision-renames.v1.0.json` — one exact-match entry per contested method+route, each carrying its oracle evidence. The files are embedded into the generator at build time (generation never reads the 22 MB oracle), applied only when `GeneratorConfig.UseCollisionData` is set, and the script's `-Validate` mode fails if the checked-in files drift from a fresh derivation. Two routes in all of v1.0 are **deferred cross-path merges** — the published SDK serves `Get-MgGroupPhoto` from both `/photo` and `/photos`, and `Get-MgShareListItem` from both `/listItem` and `/list/items`, as parameter-set variants of one cmdlet; the generator keeps the singleton side and suppresses the collection side until cross-path parameter sets land (see [edge-cases/crosspath-merge-edge-cases.md](edge-cases/crosspath-merge-edge-cases.md)). With the derived data applied, a full v1.0 generation across all 39 modules produces zero collisions. +On top of the curated entries sits a **derived** layer: `tools/Derive-CollisionResolutions.ps1` replays every route from the checked-in collision inventory (`data/collision-inventory.v1.0.txt`, captured with `--no-collision-data`) against the shipped-command inventory and emits `data/collision-suppressions.v1.0.json` and `data/collision-renames.v1.0.json` — one exact-match entry per contested method+route, each carrying its oracle evidence. The files are embedded into the generator at build time (generation never reads the 22 MB oracle), applied only when `GeneratorConfig.UseCollisionData` is set, and the script's `-Validate` mode fails if the checked-in files drift from a fresh derivation. Two routes in all of v1.0 are **deferred cross-path merges** — the published SDK serves `Get-MgGroupPhoto` from both `/photo` and `/photos`, and `Get-MgShareListItem` from both `/listItem` and `/list/items`, as parameter-set variants of one cmdlet; the generator keeps the singleton side and suppresses the collection side until cross-path parameter sets land (see [docs/edge-cases/crosspath-merge-edge-cases.md](docs/edge-cases/crosspath-merge-edge-cases.md)). With the derived data applied, a full v1.0 generation across all 39 modules produces zero collisions. ## The one subtle part: list + item GET become one cmdlet @@ -100,8 +100,18 @@ Whatever the shape, a generated cmdlet has the same skeleton: - **Auth**: every cmdlet takes an optional `-AccessToken`; without it, the cmdlet uses the active `Connect-MgGraph` session. (The shared auth helpers are written once per module into `Shared.g.cs`.) - **GETs** expose the OData query options the operation supports — `-Filter`, `-Property` (alias`-Select`), `-Sort` (alias `-OrderBy`), `-Top`, `-Skip`, `-Count`. -- **`New`/`Update`** flatten the request body's top-level primitive properties into parameters (`-Subject`, `-IsRead`, …). Nested/complex properties are skipped, with one special case: - `passwordProfile` is exposed as `-Password`/`-ForceChangePasswordNextSignIn` because creating a user requires it. `Update` also re-fetches after a `204 No Content` so it still returns the updated object. +- **`New`/`Update`** bind the request body's properties as parameters. Primitives flatten directly + (`-Subject`, `-IsRead`, …); a referenced model binds as its kiota type, so + `New-MgUser -PasswordProfile @{ Password = '...' }` works — PowerShell converts the hashtable on + binding. Referenced enums bind as the generated enum (`-Importance high`), and formatted strings + bind as the CLR type kiota uses (`date-time` → `DateTimeOffset`, `uuid` → `Guid`, `base64url` → + `byte[]`). A property the spec gives no type at all — which kiota emits as `UntypedNode` — takes + an ordinary PowerShell value (`-Maximum 100`, `-ContentInfo @{ … }`) and is converted on + assignment. Navigation properties, `id`, `additionalData` and `readOnly` properties are + deliberately excluded: they are relationships or serializer infrastructure, not body fields. + `Update` also re-fetches after a `204 No Content` so it still returns the updated object. + See [docs/body-property-binding.md](docs/body-property-binding.md) for the full mapping, the + `date`/`time` input contract, and what remains unbound. - **`New`/`Update`/`Remove`** are gated by `ShouldProcess`, so `-WhatIf` and `-Confirm` work. - **The actual request** is the Kiota client's fluent chain built from the path: `client.Users[UserId].Messages[MessageId].GetAsync(...)`. @@ -154,23 +164,68 @@ dotnet run --project tools/WrapperGenerator -- ` `-d` is the spec, `-o` the output folder, `-n` the namespace of the step-1 client the wrappers call. Each `--include-path` is a glob with an optional `#METHOD,METHOD` filter; omit them to generate every operation in the document. Output: `Shared.g.cs`, one `*.g.cs` per cmdlet (in a namespace derived from `-n` by dropping its trailing `.Client`, e.g. `-n Microsoft.Graph.PowerShell.Mail.Client` emits into `Microsoft.Graph.PowerShell.Mail`), and a small `kiota-lock.json` noting the source spec. -**Test** — two layers: +## The committed output + +The generated modules are checked in under `src/{Module}/{v1.0|beta}/wrapper/`, one +self-contained project per module and API version: + +``` +src/Mail/v1.0/wrapper/ + Client/ kiota client (models + request builders) + Cmdlets/ the wrapper cmdlets, one *.g.cs each, plus Shared.g.cs + Microsoft.Graph.Wrapper.Mail.csproj compiles both into one assembly +``` + +Everything needed to build is in that folder, so no generation step is required to try it: ```powershell -# 1. Naming rules pinned to published Microsoft.Graph names +dotnet build src/Mail/v1.0/wrapper # produces the dll + psd1 under bin/ +Import-Module src/Mail/v1.0/wrapper/bin/Release/net10.0/Microsoft.Graph.Wrapper.Mail.psd1 +``` + +To regenerate it after a generator change — this rewrites the committed folder in place, so the +diff shows exactly what the change did to the output: + +```powershell +.\tools\Build-WrapperModule.ps1 -Module Mail -IntoSource -Configuration Release +.\tools\New-WrapperOutputManifest.ps1 # refresh docs/WrapperCmdlets-V1.0*.csv +``` + +`docs/WrapperCmdlets-V1.0.csv` is the reviewable inventory of that output — one row per emitted +cmdlet with its module, verb, noun and request path — with per-module totals in +`docs/WrapperCmdlets-V1.0-Summary.csv`. The generated tree is far larger than GitHub renders in +a diff, so those two files, not the tree, are what a reviewer reads. + +**Test** — several layers, each proving something the others cannot: + +```powershell +# 1. Naming and classification rules pinned to published Microsoft.Graph names dotnet test tools/WrapperGenerator.Tests -# => Passed! - Failed: 0, Passed: 120, Total: 120 +# => Passed! - Failed: 0, Passed: 148, Total: 148 # 2. Parity gate: generate, then check every cmdlet name against Graph's own command inventory .\tools\Compare-WrapperCmdletNames.ps1 -GeneratedPath # => Mail [v1.0]: 4 of 4 cmdlets match the oracle ... EXIT CODE: 0 + +# 3. Compile gate: every module builds against the kiota client it was generated with +.\tools\Build-WrapperModule.ps1 -Module -Configuration Release + +# 4. Omission oracle: every settable kiota body member is bound or cited by a named policy +.\tools\Test-BodyBindingCoverage.ps1 + +# 5. Runtime gate: the module imports and each bound shape accepts what a person would type +.\tools\Test-WrapperModule.ps1 -Module -Configuration Release ``` -The unit tests guard the naming rules (their expected values are real published names from `src/Authentication/Authentication/custom/common/MgCommandMetadata.json`). The parity gate checks actual generated output against that same inventory; names on the deliberate-corrections list ([edge-cases/naming-edge-cases.md](edge-cases/naming-edge-cases.md)) are reported as `[CORRECTED]` instead of failing. There is **no** test yet that the generated cmdlets *compile* — that needs step 1's client to compile against. +The unit tests guard the naming and classification rules (their expected values are real published names from `src/Authentication/Authentication/custom/common/MgCommandMetadata.json`). The parity gate checks actual generated output against that same inventory; names on the deliberate-corrections list ([docs/edge-cases/naming-edge-cases.md](docs/edge-cases/naming-edge-cases.md)) are reported as `[CORRECTED]` instead of failing. + +The generated cmdlets **are** compiled: `Build-WrapperModule.ps1` builds each module against the kiota client it was generated with, the only authority on whether an emitted parameter's CLR type matches the member it assigns. Compilation cannot see an *omitted* member, so the omission oracle exists separately; neither can see whether PowerShell converts a value at runtime, so the runtime gate exists separately again. + +**Known failing gate at this commit:** the naming parity gate reports 5,689 of 7,434 comparable names matching the published SDK. Those mismatches predate this change (this commit's only naming edit is a doc-path comment) and are tracked for a separate oracle-derived naming change; they are disclosed here rather than hidden from the gate list. ## Gaps / not done yet -- **Output isn't committed into `src/{Module}/`.** `tools/Build-WrapperModule.ps1` now turns a module into an importable build under `artifacts/` (kiota client + wrappers + csproj + PSD1; `tools/Test-WrapperModule.ps1` smoke-tests it), with generation reading the Kiota-compatible docs (`openApiDocs_KiotaCompat`) by default. Cmdlets now emit into a per-module namespace (not `MgPoC`) and the generated csproj references Authentication by a relative path, so both are ready to move; the exact target folder under `src/{Module}/{v1.0|beta}/` is still open — the existing AutoRest modules' `.gitignore` there excludes a folder literally named `generated`, so the wrapper output needs a different folder name or that pattern needs updating, or a commit there would silently produce an empty diff. +- **Only v1.0 output is committed.** The beta docs exist (`openApiDocs_KiotaCompat/beta`) but no beta output is generated or checked in yet; the layout already accommodates it at `src/{Module}/beta/wrapper/`. - **No runtime base classes or real auth flow.** Shared paging, a proper `Connect-MgGraph`/session integration, and base cmdlet classes are a later phase. -- **Body binding is shallow** — top-level primitive properties only; no nested/complex types beyond the `passwordProfile` special case. -- **Some operation shapes aren't generated** — `$count`/`$ref`/`$value`, delta, OData actions/functions, and cast endpoints. +- **Body binding covers every shape reaching the classifier** — the sweep reports 0 unbound properties across all 38 specs, and the omission oracle reports 0 failures across 2,633 body-writing cmdlets. That is a statement about the operations that generate, not about v1.0 (see the next bullet). Classifications for shapes that do not occur (inline objects and enums, genuine unions, dictionaries, unresolvable references, unknown formats) are retained so a future corpus change is reported rather than silently mis-bound. `tools/Measure-BodyPropertyCoverage.ps1` counts them; [docs/edge-cases/body-binding-edge-cases.md](docs/edge-cases/body-binding-edge-cases.md) records each with its exit criteria. +- **Only 57.8% of v1.0 operations generate.** Of 14,131 operations across the 38 specs: 8,164 become cmdlets, 767 are suppressed because the published SDK ships no cmdlet for them, and 5,200 are unsupported — 3,495 OData path segments (`$count`/`$ref`/`$value`, delta, cast, and parameterized functions), 1,528 POST actions whose request schema is not a named entity, 93 PUT, 78 media/stream, 6 unresolvable responses. The three populations sum to 14,131 by construction; emitted files are not operations (9,608 files include 1,444 GET dispatchers that issue no request of their own). diff --git a/tools/WrapperGenerator/SchemaProperties.cs b/tools/WrapperGenerator/SchemaProperties.cs index 30d3ac23023..af6f9146955 100644 --- a/tools/WrapperGenerator/SchemaProperties.cs +++ b/tools/WrapperGenerator/SchemaProperties.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.OpenApi; @@ -12,18 +12,118 @@ public sealed record CmdletProperty(string OpenApiName, string PascalName, strin public string ParameterName { get; init; } = PascalName; } -// Maps a body schema's top-level primitive properties onto cmdlet parameters. Deliberately -// shallow, per team decision: nested complex properties (assignedLicenses, employeeOrgData, -// and the like) are skipped rather than modeled. Two special cases: "id" is excluded because -// the server assigns it, and passwordProfile is flagged separately via HasPasswordProfile -// because creating a user requires it. +// A body property whose type is a model in the spec's component schemas, bound as a parameter +// typed as the corresponding kiota model class. ReferenceId is the component schema key +// ("microsoft.graph.passwordProfile"); the generation service turns it into a CLR type name, +// because only it knows kiota's namespace and reserved-name rules. +// IsEnum carries through because an enum is a value type: kiota declares a collection of one +// with nullable elements (List), unlike a collection of models. +public sealed record ComplexProperty(string OpenApiName, string PascalName, string ReferenceId, bool IsArray, bool IsEnum) +{ + public string ParameterName { get; init; } = PascalName; +} + +// A property the spec gives no type, which kiota emits as UntypedNode. It binds as object and +// is converted at assignment, so a caller passes an ordinary PowerShell value. +public sealed record UntypedProperty(string OpenApiName, string PascalName) +{ + public string ParameterName { get; init; } = PascalName; +} + +// The same after parameter-name collision resolution. LocalName names the converted value in +// the emitted assignment; it is derived from the parameter so two properties in one cmdlet +// cannot declare the same local. +public sealed record UntypedParameter(string PascalName, string ParameterName) +{ + public string LocalName => "untyped" + ParameterName; +} + +// A complex property after the generation service has turned its ReferenceId into a kiota CLR +// type name. Emission takes this rather than ComplexProperty so a parameter cannot be emitted +// with an unresolved type. +public sealed record ComplexParameter(string PascalName, string ParameterName, string TypeName, bool IsArray, bool IsEnum) +{ + // The declared parameter type. An array of enums needs nullable elements to assign to + // kiota's List; an array of models must not, matching List. + public string ElementNullableTypeName => IsArray + ? TypeName + (IsEnum ? "?[]" : "[]") + : TypeName; +} + +// Why a property could not be bound. Each value is a distinct spec shape rather than a generic +// "unsupported", so a sweep says which shapes are worth implementing next instead of just how +// many were missed. +public enum UnsupportedShape +{ + InlineEnum, // enum declared inline; kiota synthesises the type name from the parent + UnknownFormat, // a format with no verified kiota CLR mapping + InlineObject, // anonymous object; kiota synthesises the type name from the parent + Union, // anyOf/oneOf that is a real choice, not the numeric/INF encoding + Dictionary, // free-form map (additionalProperties) + Unresolvable, // an array with no item schema, or a reference with no bindable target +} + +public sealed record UnsupportedProperty(string OpenApiName, UnsupportedShape Shape, bool IsRequired); + +// Why a property is deliberately not a parameter. Each is a protocol or framework rule, never a +// Graph corpus special case, and each is named so an external check can tell a policy exclusion +// apart from an omission. +public enum ExclusionPolicy +{ + ServerAssignedId, // "id" is assigned by the service + ODataControlData, // "@"-prefixed, e.g. @odata.type; kiota's serializer supplies it + KiotaAdditionalData, // the IAdditionalDataHolder bag every kiota model already exposes + ReadOnlySchema, // readOnly: true - the OpenAPI signal for server-managed + NavigationProperty, // x-ms-navigationProperty - a relationship with its own request path +} + +public sealed record ExcludedProperty(string OpenApiName, ExclusionPolicy Policy); + +// The full classification of one request body. PropertiesSeen is counted independently of the +// buckets, so Scalars + Complex + Unsupported + Excluded == PropertiesSeen is a real +// invariant rather than an identity that holds by construction; Classify throws if it breaks. +public sealed record BodyProperties( + IReadOnlyList Scalars, + IReadOnlyList Complex, + IReadOnlyList Untyped, + IReadOnlyList Unsupported, + IReadOnlyList Excluded, + int PropertiesSeen); + +// Maps a request body schema onto cmdlet parameters. Scalars bind directly; properties whose +// type is a referenced model bind as that model's kiota type (PowerShell coerces a hashtable +// into it). Shapes whose kiota type name cannot be derived from the spec - inline objects and +// enums, unions, dictionaries - are reported rather than guessed, because a wrong type name is +// a compile error in the generated module. public static class SchemaProperties { - public static IReadOnlyList ExtractPrimitiveProperties(IOpenApiSchema schema) + // resolveReference maps a component schema key to its schema, so a $ref can be inspected + // before deciding what it is: a reference to microsoft.graph.importance is a string enum, + // not a model, and binding it as a model would not compile. + public static BodyProperties Classify(IOpenApiSchema schema, Func resolveReference) { ArgumentNullException.ThrowIfNull(schema); - var result = new List(); + ArgumentNullException.ThrowIfNull(resolveReference); + + var scalars = new List(); + var complex = new List(); + var untyped = new List(); + var unsupported = new List(); + var excluded = new List(); var seen = new HashSet(StringComparer.Ordinal); + var required = new HashSet(StringComparer.Ordinal); + + void CollectRequired(IOpenApiSchema s) + { + foreach (var inherited in s.AllOf ?? []) + CollectRequired(inherited); + if (s.Required is { } names) + { + foreach (var name in names) + required.Add(name); + } + } + CollectRequired(schema); void Walk(IOpenApiSchema s) { @@ -32,82 +132,351 @@ void Walk(IOpenApiSchema s) foreach (var (name, propSchema) in s.Properties ?? new Dictionary()) { - if (IsProtocolOrServerManagedProperty(name, propSchema) || !seen.Add(name)) + if (!seen.Add(name)) continue; - if (IsPlainScalar(propSchema)) + if (TryGetExclusionPolicy(name, propSchema) is { } policy) { - result.Add(new CmdletProperty(name, ToKiotaPropertyName(name), MapPsType(propSchema), IsArray: false)); + excluded.Add(new ExcludedProperty(name, policy)); + continue; } - else if (propSchema.Type == JsonSchemaType.Array && propSchema.Items is { } items && IsPlainScalar(items)) + + var isRequired = required.Contains(name); + var pascal = ToKiotaPropertyName(name); + + switch (ClassifyProperty(propSchema, resolveReference)) { - result.Add(new CmdletProperty(name, ToKiotaPropertyName(name), MapPsType(items) + "[]", IsArray: true)); + case ScalarShape scalarShape: + scalars.Add(new CmdletProperty(name, pascal, scalarShape.PsTypeName, scalarShape.IsArray)); + break; + case ModelShape modelShape: + // Requiredness is deliberately not carried here: every bound parameter + // is optional, and Graph's schemas do not mark requiredness usefully. + // The measurement behind that is in docs/body-property-binding.md. + complex.Add(new ComplexProperty(name, pascal, modelShape.ReferenceId, modelShape.IsArray, modelShape.IsEnum)); + break; + case UntypedShape: + untyped.Add(new UntypedProperty(name, pascal)); + break; + case UnsupportedNativeShape u: + unsupported.Add(new UnsupportedProperty(name, u.Shape, isRequired)); + break; } } } Walk(schema); - return result; + + // seen counts every distinct property the walk reached, without reference to where it + // was routed. A property that fell through the shape switch would show up here and + // nowhere else, which is precisely the failure a summed total could never reveal. + var accountedFor = scalars.Count + complex.Count + untyped.Count + unsupported.Count + excluded.Count; + if (seen.Count != accountedFor) + { + throw new InvalidOperationException( + $"Body property classification is not exhaustive: reached {seen.Count} properties but accounted for " + + $"{accountedFor} (scalar {scalars.Count} + model {complex.Count} + untyped {untyped.Count} + unsupported {unsupported.Count} + excluded {excluded.Count})."); + } + + return new BodyProperties(scalars, complex, untyped, unsupported, excluded, seen.Count); + } + + private abstract record PropertyShape; + private sealed record UntypedShape : PropertyShape; + private sealed record ScalarShape(string PsTypeName, bool IsArray) : PropertyShape; + private sealed record ModelShape(string ReferenceId, bool IsArray, bool IsEnum) : PropertyShape; + private sealed record UnsupportedNativeShape(UnsupportedShape Shape) : PropertyShape; + + private static PropertyShape ClassifyProperty(IOpenApiSchema propSchema, Func resolveReference) + { + // An array is classified by its item schema, at the same single level of nesting the + // scalar case allows - arrays of arrays are not a shape Graph request bodies use. + if ((propSchema.Type & ~JsonSchemaType.Null) == JsonSchemaType.Array) + { + if (propSchema.Items is not { } items) + return new UnsupportedNativeShape(UnsupportedShape.Unresolvable); + return ClassifyLeaf(items, resolveReference, isArray: true); + } + + return ClassifyLeaf(propSchema, resolveReference, isArray: false); + } + + private static PropertyShape ClassifyLeaf(IOpenApiSchema leaf, Func resolveReference, bool isArray) + { + if (TryMapScalar(leaf, out var scalarType, out var badFormat)) + return new ScalarShape(ArrayAware(scalarType, isArray), isArray); + if (badFormat) + return new UnsupportedNativeShape(UnsupportedShape.UnknownFormat); + + // A reference, either directly or as the single meaningful branch of a nullable union. + // An enum reference resolves the same way a model does - kiota emits both as named types + // in the models namespace - so both bind through the same path. + var referenceId = leaf.GetReferenceId() ?? SingleReferenceOfNullableUnion(leaf); + if (referenceId is not null) + { + var target = resolveReference(referenceId); + if (target is null) + return new UnsupportedNativeShape(UnsupportedShape.Unresolvable); + var targetType = target.Type & ~JsonSchemaType.Null; + // Graph model schemas are objects; some declare no type at all and are objects by + // virtue of carrying properties or an allOf chain. Enums are named types too. + var isEnum = IsEnumSchema(target); + if (isEnum || targetType == JsonSchemaType.Object || targetType is null) + return new ModelShape(referenceId, isArray, isEnum); + // A reference to a bare scalar carries no kiota type of its own to bind to. + return new UnsupportedNativeShape(UnsupportedShape.Unresolvable); + } + + if ((leaf.AnyOf?.Count ?? 0) > 0 || (leaf.OneOf?.Count ?? 0) > 0) + { + return TryMapNumericUnion(leaf, resolveReference, out var unionType) + ? new ScalarShape(ArrayAware(new ScalarType(unionType, IsValueType: true), isArray), isArray) + : new UnsupportedNativeShape(UnsupportedShape.Union); + } + if (IsEnumSchema(leaf)) + return new UnsupportedNativeShape(UnsupportedShape.InlineEnum); + if (leaf.AdditionalProperties is not null) + return new UnsupportedNativeShape(UnsupportedShape.Dictionary); + if ((leaf.Properties?.Count ?? 0) > 0 || (leaf.Type & ~JsonSchemaType.Null) == JsonSchemaType.Object) + return new UnsupportedNativeShape(UnsupportedShape.InlineObject); + + // Nothing left to go on: no type, reference, enum, format or members - Graph writes + // these with only a description (workbookChartAxis.maximum) and kiota emits UntypedNode. + // An array is not treated this way; its element shape is decided by ClassifyProperty. + return isArray ? new UnsupportedNativeShape(UnsupportedShape.Unresolvable) : new UntypedShape(); + } + + // OData's non-finite doubles: a numeric property that may instead arrive as one of these + // sentinel strings. Their presence in a referenced enum is what identifies the encoding. + private static readonly HashSet NonFiniteNumericSentinels = + new(StringComparer.Ordinal) { "-INF", "INF", "NaN" }; + + // Graph encodes a numeric that may also carry an OData non-finite value as a union of the + // numeric, a bare string, and a reference to a string enum of the sentinels. Kiota keeps the + // numeric and drops the rest (bookingService.price generates as double?), so the numeric + // branch is the type to bind. + // + // All three conditions are required, because each rules out a different real choice: + // one numeric branch (two numerics is a choice of precision), every other branch merely + // stringish (a model or formatted-string arm would be silently discarded), and at least one + // sentinel enum (without it, "number or string" is an ordinary union whose string arm means + // something). Recognition is by enum VALUES, never by the name of the schema carrying them. + private static bool TryMapNumericUnion(IOpenApiSchema schema, Func resolveReference, out string mapped) + { + mapped = string.Empty; + var branches = schema.AnyOf ?? schema.OneOf; + if (branches is null) + return false; + + var sawSentinelEnum = false; + foreach (var branch in branches) + { + if ((branch.Type & ~JsonSchemaType.Null) is JsonSchemaType.Integer or JsonSchemaType.Number) + { + if (mapped.Length > 0) + return false; // two numerics is a genuine choice + mapped = MapNumericType(branch); + continue; + } + if (!IsStringishAlternative(branch, resolveReference, ref sawSentinelEnum)) + return false; + } + return mapped.Length > 0 && sawSentinelEnum; } + private static bool IsStringishAlternative(IOpenApiSchema branch, Func resolveReference, ref bool sawSentinelEnum) + { + if (branch.GetReferenceId() is { } id) + { + var target = resolveReference(id); + if (target is null || !IsEnumSchema(target) || (target.Type & ~JsonSchemaType.Null) != JsonSchemaType.String) + return false; + // A referenced string enum only qualifies when it carries the sentinels; any other + // enum is a meaningful alternative, not the non-finite encoding. + if (!EnumValues(target).All(NonFiniteNumericSentinels.Contains)) + return false; + sawSentinelEnum = true; + return true; + } + if (IsNullabilityPlaceholder(branch)) + return true; + return (branch.Type & ~JsonSchemaType.Null) == JsonSchemaType.String + && string.IsNullOrEmpty(branch.Format) + && (branch.Enum?.Count ?? 0) == 0; + } + + // Enum members are JSON nodes; a quoted string node renders with quotes through ToString, + // so the value is read directly where possible and unquoted otherwise. + private static IEnumerable EnumValues(IOpenApiSchema schema) + { + foreach (var node in schema.Enum ?? []) + { + if (node is null) + continue; + string? value; + try { value = node.GetValue(); } + catch (InvalidOperationException) { value = node.ToString().Trim('"'); } + catch (FormatException) { value = node.ToString().Trim('"'); } + if (value is not null) + yield return value; + } + } + + // Graph writes a nullable complex property as anyOf[ $ref, { type: object, nullable: true } ] + // (user.passwordProfile). Only that exact shape is unwrapped: exactly one branch resolves to + // a reference and every other branch is an empty nullability placeholder. Two references, or + // a branch with real content, is a genuine union and stays unsupported rather than having + // one arm silently chosen for the caller. + private static string? SingleReferenceOfNullableUnion(IOpenApiSchema schema) + { + var branches = schema.AnyOf ?? schema.OneOf; + if (branches is null || branches.Count == 0) + return null; + + string? referenceId = null; + foreach (var branch in branches) + { + var id = branch.GetReferenceId(); + if (id is not null) + { + if (referenceId is not null) + return null; + referenceId = id; + continue; + } + if (!IsNullabilityPlaceholder(branch)) + return null; + } + return referenceId; + } + + // A branch that adds nullability and nothing else: no reference, no members, no enum, no + // items, no format. + private static bool IsNullabilityPlaceholder(IOpenApiSchema schema) => + (schema.Properties?.Count ?? 0) == 0 + && (schema.Enum?.Count ?? 0) == 0 + && schema.Items is null + && schema.AdditionalProperties is null + && string.IsNullOrEmpty(schema.Format) + && (schema.AnyOf?.Count ?? 0) == 0 + && (schema.OneOf?.Count ?? 0) == 0; + + private static bool IsEnumSchema(IOpenApiSchema schema) => (schema.Enum?.Count ?? 0) > 0; + // A body property whose Pascal name matches a path parameter would emit a duplicate C# // property (PATCH /devices/{device-id} has a path id AND a body property "deviceId" — // different values: the URL takes the object id, the body carries Entra's deviceId). // The published SDK keeps both reachable by suffixing the body one with "1" // (Update-MgDevice ships -DeviceId and -DeviceId1); reproduce that convention rather - // than dropping a settable property. - public static IReadOnlyList ResolveParameterNameCollisions( - IReadOnlyList properties, IReadOnlyList pathParamNames) + // than dropping a settable property. Scalars and complex properties share one parameter + // namespace, so they are resolved together. + public static (IReadOnlyList Scalars, IReadOnlyList Complex, IReadOnlyList Untyped) ResolveParameterNameCollisions( + IReadOnlyList scalars, IReadOnlyList complex, IReadOnlyList untyped, IReadOnlyList pathParamNames) { - ArgumentNullException.ThrowIfNull(properties); + ArgumentNullException.ThrowIfNull(scalars); + ArgumentNullException.ThrowIfNull(complex); + ArgumentNullException.ThrowIfNull(untyped); ArgumentNullException.ThrowIfNull(pathParamNames); + var taken = new HashSet(pathParamNames, StringComparer.Ordinal); - return properties - .Select(p => taken.Contains(p.PascalName) ? p with { ParameterName = p.PascalName + "1" } : p) - .ToList(); - } + string Unique(string pascal) + { + var candidate = pascal; + while (!taken.Add(candidate)) + candidate += "1"; + return candidate; + } - // passwordProfile is a nested complex type, so ExtractPrimitiveProperties skips it, but - // Graph requires it to create a user. This flag lets the emitter add the two flattened - // parameters (-Password, -ForceChangePasswordNextSignIn) that make New-MgUser usable. - public static bool HasPasswordProfile(IOpenApiSchema schema) - { - ArgumentNullException.ThrowIfNull(schema); - if (schema.Properties?.ContainsKey("passwordProfile") ?? false) - return true; - return schema.AllOf?.Any(HasPasswordProfile) ?? false; + var resolvedScalars = scalars.Select(p => p with { ParameterName = Unique(p.PascalName) }).ToList(); + var resolvedComplex = complex.Select(p => p with { ParameterName = Unique(p.PascalName) }).ToList(); + var resolvedUntyped = untyped.Select(p => p with { ParameterName = Unique(p.PascalName) }).ToList(); + return (resolvedScalars, resolvedComplex, resolvedUntyped); } - // A "format" on a string (date-time, uuid, byte, ...) means Kiota maps it to a non-string - // CLR type, and an enum-valued string becomes a real enum type. Both are left out rather - // than guessing Kiota's mapping and risking a type mismatch. Schema.Type is a flags enum - // and nullable unions set the Null bit, so it is masked off before comparing. - private static bool IsPlainScalar(IOpenApiSchema schema) => (schema.Type & ~JsonSchemaType.Null) switch + // The CLR type kiota gives a formatted string. Every entry is taken from a generated Graph + // client rather than from kiota's documentation, because only the generated member type has + // to match: a wrong name is a compile error in the module. Fully qualified so a Graph model + // called Date or Time cannot capture the name, and because the emitted cmdlets do not import + // Microsoft.Kiota.Abstractions. + // IsValueType travels with the mapping rather than in a parallel set: kiota declares a + // collection of a value type with nullable elements and a reference type without, so the two + // facts have to move together or a new mapping silently gets the wrong element contract. + private sealed record ScalarType(string Name, bool IsValueType); + + private static readonly Dictionary StringFormatTypes = new(StringComparer.OrdinalIgnoreCase) { - JsonSchemaType.String => string.IsNullOrEmpty(schema.Format) && (schema.Enum?.Count ?? 0) == 0, - JsonSchemaType.Boolean or JsonSchemaType.Integer or JsonSchemaType.Number => true, - _ => false, + ["date-time"] = new("global::System.DateTimeOffset", IsValueType: true), + ["uuid"] = new("global::System.Guid", IsValueType: true), + ["duration"] = new("global::System.TimeSpan", IsValueType: true), + ["date"] = new("global::Microsoft.Kiota.Abstractions.Date", IsValueType: true), + ["time"] = new("global::Microsoft.Kiota.Abstractions.Time", IsValueType: true), + ["base64url"] = new("byte[]", IsValueType: false), // an array is a reference type + ["binary"] = new("byte[]", IsValueType: false), // no Stream members exist in the generated clients }; + // "string" -> "string[]", "int" -> "int?[]", and a non-array passes through unchanged. + // ToList() on T[] yields List, which will not assign to kiota's List, so an element + // that is a value type must be declared nullable. + private static string ArrayAware(ScalarType scalar, bool isArray) => + !isArray ? scalar.Name + : scalar.IsValueType ? scalar.Name + "?[]" + : scalar.Name + "[]"; + + // Maps a scalar schema to its CLR type. badFormat distinguishes "not a scalar at all" from + // "a scalar whose format has no verified mapping" — the latter must be reported rather than + // silently bound as string, which would compile against the wrong kiota member type. + private static bool TryMapScalar(IOpenApiSchema schema, out ScalarType mapped, out bool badFormat) + { + mapped = default!; + badFormat = false; + + // Schema.Type is a flags enum and nullable unions set the Null bit, so mask it off. + switch (schema.Type & ~JsonSchemaType.Null) + { + case JsonSchemaType.Boolean: + mapped = new ScalarType("bool", IsValueType: true); + return true; + case JsonSchemaType.Integer or JsonSchemaType.Number: + // Every numeric CLR type is a struct. + mapped = new ScalarType(MapNumericType(schema), IsValueType: true); + return true; + case JsonSchemaType.String: + // An enum-valued string is a named kiota type, not a scalar; it binds through + // its $ref like a model does. + if ((schema.Enum?.Count ?? 0) > 0) + return false; + if (string.IsNullOrEmpty(schema.Format)) + { + mapped = new ScalarType("string", IsValueType: false); + return true; + } + if (StringFormatTypes.TryGetValue(schema.Format, out var formatted)) + { + mapped = formatted; + return true; + } + badFormat = true; + return false; + default: + return false; + } + } + // Numeric mapping: when a format is present it decides the CLR type, mirroring Kiota's // own mapping, so a wrapper parameter always matches the Kiota model property it is // assigned to. Graph's docs declare Edm.Int32 as "type: number, format: int32" — going by - // the type alone would emit double? against Kiota's int? and not compile. Without a - // format, integer stays int and number stays double (fraction and 64-bit safety). - private static string MapPsType(IOpenApiSchema schema) => (schema.Type & ~JsonSchemaType.Null) switch + // the type alone would emit double? against Kiota's int? and not compile. int16 is absent + // deliberately: no generated client contains a short member, so kiota widens it to int. + // Without a format, integer stays int and number stays double (fraction and 64-bit safety). + private static string MapNumericType(IOpenApiSchema schema) => schema.Format?.ToLowerInvariant() switch { - JsonSchemaType.String => "string", - JsonSchemaType.Boolean => "bool", - JsonSchemaType.Integer or JsonSchemaType.Number => schema.Format?.ToLowerInvariant() switch - { - "int64" => "long", - "int32" => "int", - "float" => "float", - "double" => "double", - "decimal" => "decimal", - _ => (schema.Type & ~JsonSchemaType.Null) == JsonSchemaType.Integer ? "int" : "double", - }, - _ => "string", + "int64" => "long", + "int32" => "int", + "float" => "float", + "double" => "double", + "decimal" => "decimal", + "uint8" => "byte", // kiota emits byte? (rgbColor.r/g/b) + _ => (schema.Type & ~JsonSchemaType.Null) == JsonSchemaType.Integer ? "int" : "double", }; // Kiota cleans property symbols when generating model members: underscores are dropped @@ -122,8 +491,23 @@ private static string ToKiotaPropertyName(string openApiName) // Excludes properties a caller cannot or should not set. "id" is server-assigned. // "@"-prefixed names like "@odata.type" are OData control data that Kiota's serializer - // fills in from the model type, and they are not legal C# identifiers anyway. ReadOnly is - // the general OpenAPI signal for server-managed. Future exclusions of this kind belong here. - private static bool IsProtocolOrServerManagedProperty(string name, IOpenApiSchema propSchema) => - name == "id" || name.StartsWith('@') || propSchema.ReadOnly; + // fills in from the model type, and they are not legal C# identifiers anyway. + // "additionalData" is the open-type bag every kiota model already exposes through + // IAdditionalDataHolder as IDictionary; where a spec also declares it (for + // example security.alertV2) kiota does not emit a second member, so binding it would assign + // a model type to the interface's dictionary and fail to compile. ReadOnly is the general + // OpenAPI signal for server-managed. Navigation properties are relationships (user.manager, + // user.messages), addressed through their own request paths and not settable in a body; + // Graph marks them with x-ms-navigationProperty and does NOT set readOnly, so that + // extension is the only signal that keeps them out. + private static ExclusionPolicy? TryGetExclusionPolicy(string name, IOpenApiSchema propSchema) => + name switch + { + "id" => ExclusionPolicy.ServerAssignedId, + "additionalData" => ExclusionPolicy.KiotaAdditionalData, + _ when name.StartsWith('@') => ExclusionPolicy.ODataControlData, + _ when propSchema.ReadOnly => ExclusionPolicy.ReadOnlySchema, + _ when propSchema.Extensions?.ContainsKey("x-ms-navigationProperty") ?? false => ExclusionPolicy.NavigationProperty, + _ => null, + }; } diff --git a/tools/WrapperGenerator/Singularizer.cs b/tools/WrapperGenerator/Singularizer.cs index 098a7ff6873..752fe839cec 100644 --- a/tools/WrapperGenerator/Singularizer.cs +++ b/tools/WrapperGenerator/Singularizer.cs @@ -100,7 +100,7 @@ public static string SingularizeWord(string word) return word[..^2]; // Businesses -> Business, Mailboxes -> Mailbox if (word.EndsWith("ss", StringComparison.Ordinal) || word.EndsWith("us", StringComparison.Ordinal) || word.EndsWith("is", StringComparison.Ordinal)) return word; // Access, Status, Analysis stay put; keeping "Whois" is a deliberate - // fix of shipped ...HostWhoi (edge-cases/naming-edge-cases.md) + // fix of shipped ...HostWhoi (docs/edge-cases/naming-edge-cases.md) if (word.EndsWith('s')) return word[..^1]; // Messages -> Message, Plans -> Plan return word; diff --git a/tools/WrapperGenerator/StderrLogger.cs b/tools/WrapperGenerator/StderrLogger.cs index 4f863fa9f05..6a2b46aee55 100644 --- a/tools/WrapperGenerator/StderrLogger.cs +++ b/tools/WrapperGenerator/StderrLogger.cs @@ -5,13 +5,14 @@ namespace WrapperGenerator; // Minimal stderr logger for CLI runs, so the generation service's skip diagnostics (for // example unsupported OData path shapes) are actually visible without taking a -// console-logging package dependency. Warning and above only: the per-file "Wrote ..." -// chatter stays quiet, and Program prints its own one-line summary. -internal sealed class StderrLogger : ILogger +// console-logging package dependency. Defaults to Warning and above: the per-file "Wrote ..." +// chatter stays quiet, and Program prints its own one-line summary. --log-level lowers the +// threshold to surface the per-property diagnostics the coverage sweep reads. +internal sealed class StderrLogger(LogLevel minimumLevel = LogLevel.Warning) : ILogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; - public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Warning; + public bool IsEnabled(LogLevel logLevel) => logLevel >= minimumLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { diff --git a/tools/WrapperGenerator/docs/body-property-binding.md b/tools/WrapperGenerator/docs/body-property-binding.md new file mode 100644 index 00000000000..7252247c585 --- /dev/null +++ b/tools/WrapperGenerator/docs/body-property-binding.md @@ -0,0 +1,241 @@ +# Request-body property binding + +How a request body's properties become cmdlet parameters, what each spec shape maps to, and +what is deliberately not bound. This is the durable record behind those decisions; the code +comments state the current rule, the measurements and reasoning live here. + +## The authoritative chain + +``` +OpenAPI shape -> generated Kiota member -> emitted parameter + assignment +``` + +The generated Kiota member is the contract that matters. Every type in the mapping table below +was read off a generated Graph client, not from documentation: a parameter whose CLR type +disagrees with the member it assigns is a compile error in the module, so the generated code is +the only authority worth trusting. + +The published (AutoRest) SDK is **not** part of this chain. It is a useful reference for cmdlet +and parameter *naming*, but its `IMicrosoftGraph*` interfaces are a different type system and +must never be used to decide a Kiota parameter type. + +## Classification outcomes + +Every property reached by the classifier lands in exactly one bucket. `Classify` counts the +properties it reaches independently of the buckets and throws if they disagree, so a shape that +fell through the switch fails generation rather than quietly disappearing. + +| Outcome | Meaning | +|---|---| +| scalar | bound as a CLR scalar (see mapping) | +| model | bound as a named Kiota type - a model class or an enum | +| excluded | deliberately not a parameter, under a named policy | +| unsupported | a shape with no verified Kiota type; reported per property | + +## Type mapping + +### Referenced types + +A `$ref` is resolved before it is classified: a reference is not automatically an object. +`microsoft.graph.importance` resolves to a string enum, and binding it as a model class would +not compile. Enums and models then bind through the same path, because Kiota emits both as +named types in the models namespace (`Models/Importance.cs` contains `public enum Importance`). + +The CLR name comes from `ResolveModelTypeName`, which already encodes Kiota's reserved-name +renames (`File` -> `FileObject`) and sub-namespace moves. There is deliberately no second +resolver. + +A nullable complex property is written by Graph as `anyOf[$ref, {type: object, nullable: true}]` +and is unwrapped only in that exact shape: exactly one branch resolves to a reference and every +other branch is an empty nullability placeholder. Two references, or a branch with real content, +is a genuine union and stays unsupported rather than having an arm chosen for the caller. + +### Scalars + +| OpenAPI | Kiota member type | Verified against | +|---|---|---| +| `boolean` | `bool?` | ubiquitous | +| `integer`/`number`, `int32` | `int?` | `mailFolder.childFolderCount` | +| `integer`/`number`, `int64` | `long?` | `drive.quotaUsed` | +| `number`, `float` / `double` / `decimal` | `float?` / `double?` / `decimal?` | ubiquitous | +| `integer`, `uint8` | `byte?` | `rgbColor.r/g/b` | +| `integer`, `int16` | `int?` | no `short` member exists in any generated client - Kiota widens | +| `string`, no format | `string` | ubiquitous | +| `string`, `date-time` | `DateTimeOffset?` | `user.birthday` | +| `string`, `uuid` | `Guid?` | `servicePrincipal.appId` | +| `string`, `duration` | `TimeSpan?` | `event.duration` | +| `string`, `date` | `Microsoft.Kiota.Abstractions.Date?` | `todoTask.startDate` | +| `string`, `time` | `Microsoft.Kiota.Abstractions.Time?` | `todoTask.dueTime` | +| `string`, `base64url` / `binary` | `byte[]?` | `application.logo`; no `Stream` member exists in any generated client | + +Format types are emitted fully qualified. `Date` and `Time` come from +`Microsoft.Kiota.Abstractions`, which the emitted cmdlets do not import, and qualification also +prevents a Graph model named `Date` from capturing the name. + +**Input contract for `date` and `time`.** Unlike every other scalar, these do not convert from a +string — `Date` and `Time` are Kiota's own structs and PowerShell has no string conversion for +them. They do convert from `[datetime]` (and from `[DateOnly]`/`[TimeOnly]`), which is what +`Get-Date` produces, so the usable form is: + +```powershell +-ExpirationDate (Get-Date '2026-12-31') # works +-ExpirationDate '2026-12-31' # fails to bind +``` + +Measured against a compiled module; see the runtime gate below. This is a real sharp edge and is +the reason the runtime conversion check exists as a separate gate — the parameter compiles and +satisfies the coverage oracle either way. + +An unrecognised format is reported as `UnknownFormat`, never silently bound as `string`: Kiota +would have mapped it to some other CLR type and the assignment would not compile. + +### Collections + +Kiota declares a collection of a **value** type with nullable elements and a collection of a +**reference** type without: + +```csharp +List? // value type +List? // enum - also a value type +List? // reference type +List? // reference type +``` + +`ToList()` on `T[]` yields `List`, which will not assign to `List`, so an array parameter +whose element is a value type is declared `T?[]`. Value-ness travels with each mapping rather +than in a parallel list, so a new mapping cannot acquire the wrong element contract by omission. + +This distinction was found by compiling the full 35-module population; a six-module sample +passed without it. + +### The numeric/INF union + +Graph encodes a numeric that may also carry OData's `INF`/`-INF`/`NaN` string as: + +```yaml +price: + oneOf: + - { type: number, format: double, nullable: true } + - { type: string, nullable: true } + - $ref: '#/components/schemas/ReferenceNumeric' +``` + +Kiota keeps the numeric and drops the rest (`bookingService.price` generates as `double?`), so +the numeric branch is what binds. Recognition requires all three conditions, and names no schema +or property — the referenced enum is identified by its **values**: + +1. exactly one numeric branch (two is a choice of precision); +2. every other branch merely stringish — a nullability placeholder or a plain string (a model or + formatted-string arm would otherwise be silently discarded); +3. at least one referenced string enum whose values are drawn from `-INF`, `INF`, `NaN`. + +Condition 3 is what makes this specific to the protocol encoding. Without it, an ordinary +`number | string` union — where the string arm means something — would collapse to the numeric. +All three are pinned by negative tests, none of which today's corpus exercises. + +## Exclusion policies + +These are protocol and framework rules, not Graph corpus exceptions. No endpoint, module, noun, +or incidental property name influences classification. + +| Policy | Rule | Why | +|---|---|---| +| `ServerAssignedId` | property named `id` | assigned by the service | +| `ODataControlData` | name starts with `@` | OData metadata; Kiota's serializer supplies it, and the name is not a legal C# identifier | +| `KiotaAdditionalData` | property named `additionalData` | every Kiota model already exposes this through `IAdditionalDataHolder` as `IDictionary`; binding it assigns a model type to that dictionary and fails to compile | +| `ReadOnlySchema` | `readOnly: true` | the OpenAPI signal for server-managed | +| `NavigationProperty` | `x-ms-navigationProperty: true` | a relationship addressed through its own request path, not a body field. Graph does **not** set `readOnly` on these, so the extension is the only signal that keeps them out | + +Each exclusion is emitted as a named diagnostic so an external check can distinguish a policy +exclusion from an omission without re-deriving the policy. + +## Requiredness + +No bound parameter is declared mandatory. Graph's schemas do not carry usable requiredness: +across the v1.0 specs, **10,604 of 10,742** `required:` blocks list only `@odata.type`. Any +count of "required but unbound" properties derived from the spec is therefore close to +meaningless and must not be cited as evidence that nothing important is missing. + +## Verification + +Five gates, each proving something the others cannot: + +| Gate | Proves | Cannot prove | +|---|---|---| +| `dotnet test tools/WrapperGenerator.Tests` | classification and emission rules | anything about the corpus | +| `tools/Build-WrapperModule.ps1` over every module | emitted CLR types match the generated Kiota members | that a member was omitted | +| `tools/Test-BodyBindingCoverage.ps1` | every settable member is bound or cited by a named policy | that a bound value converts at runtime | +| `tools/Test-WrapperModule.ps1` | PowerShell converts a hashtable to a model, a string to an enum, a `[datetime]` to a kiota `Date`, and 19 schema-less cases through the module's own compiled `UntypedValue` | compile-time type agreement | +| `tools/Compare-WrapperOperationInventory.ps1` | a parameter-level change did not alter which operations generate | anything about parameters | + +`tools/Measure-BodyPropertyCoverage.ps1` reports what remains unbound, by shape, as both +occurrences and distinct identities - the same inherited property repeats across every cmdlet +that binds its model, so occurrences overstate the remaining work. + +Compilation is the authority for type compatibility; the omission oracle is the authority for +omissions; runtime tests are the authority for PowerShell conversion. + +**Only some of these are independent of the classifier, and the distinction matters.** +`Test-BodyBindingCoverage.ps1` builds its expectation from the *generated kiota models* and joins +it against the *emitted parameters*, so the classifier is the subject rather than the judge — it +catches a member the classifier never mentioned. `Measure-BodyPropertyCoverage.ps1` is different: +it consumes the generator's own `Unbound`/`Excluded` diagnostics, so it reports what the +classifier says about itself. That makes it a measurement instrument, not a gate, and a zero from +it means "the classifier reported nothing unbound", never "nothing is unbound". Cite the oracle +for that claim. + +**The runtime gate refuses a stale binary.** It loads whatever is on disk, so a module last built +before the change under test would pass every check while proving nothing. `Build-` and `Test-` +both default to `Debug`, so a deliberate `-Configuration Release` build leaves a months-old +`Debug` binary in place for the test run to find — which is exactly what happened here, and it +reported green. The gate now compares the assembly's timestamp against the newest generated +source and fails with both dates rather than testing the wrong artifact. + +## Schema-less properties + +A property Graph writes with only a `description` — no type, reference, enum or format +(`workbookChartAxis.maximum`) — generates as `UntypedNode`, a base class PowerShell cannot +convert to. It binds as `object` and is converted on assignment by `UntypedValue.From` in +`Shared.g.cs`: string to `UntypedString`, integral to `UntypedInteger`/`UntypedLong`, fractional +to `UntypedDouble`/`UntypedDecimal`/`UntypedFloat`, bool to `UntypedBoolean`, array to +`UntypedArray`, hashtable recursively to `UntypedObject`. An unrecognised CLR type throws with +the type named rather than being stringified. + +**Null handling.** The published SDK's `AddIf` helper adds a value only when it is non-null and +not an empty JSON object, and no model serializer has an explicit-null path — so `{"prop": null}` +was never sendable and clearing a field that way was never possible here. Emitting an explicit +null would invent a capability the published surface does not have, so the converter omits a null +and an empty hashtable. That much is parity. + +The nested rules are an extension, and worth separating from the parity claim. AutoRest applies +`AddIf` at every level it generates — including per array element +(`autorest.powershell/powershell/llcsharp/schema/array.ts:227,235`) — so "drop, don't send null" +is its consistent behaviour rather than a top-level special case. But a caller-supplied untyped +bag has no analogue in the published SDK: every AutoRest body is a generated type, so there is no +precedent for what a null *inside* a hashtable should do. Extending the same rule is a choice, not +an inherited one. The converter therefore also drops a null nested among other members while its +siblings survive, drops a null array element, and omits an object whose members all drop out. +This is the wrapper's own input contract; it is pinned by the runtime gate rather than asserted. + +## Residual debt + +**None among the operations the generator emits: the sweep reports 0 unbound properties across all 38 specs, and the oracle 0 failures across 2,633 body-writing cmdlets.** Of the 14,131 operations in those specs only 8,164 (57.8%) generate - 767 are suppressed (the published SDK ships no cmdlet) and 5,200 are unsupported (path segments, actions, PUT, streams) - so a zero here says nothing about an operation refused upstream. The +classifications for shapes that do not occur — `Union`, `UnknownFormat`, `InlineObject`, +`InlineEnum`, `Dictionary`, `Unresolvable` — are retained deliberately so a future corpus change +is reported rather than silently mis-bound. + +See [edge-cases/body-binding-edge-cases.md](edge-cases/body-binding-edge-cases.md) for each +shape, its population, and its exit criteria. + +## Measured effect + +| | Occurrences | Distinct | +|---|---:|---:| +| Unbound before this work | 4,466 | 2,426 | +| After typed models, enums, formats and unions | 28 | 20 | +| After schema-less properties | **0** | **0** | + +`New-MgUser` went from 59 parameters to 82 over the same change **in a freshly generated tree**, +and the operation inventory is unchanged at 9,608 cmdlets — these slices altered which +*properties* bind, never which *operations* generate. The committed output under `src/` predates +this work and still shows 59; it has to be regenerated before the same figure applies there. diff --git a/tools/WrapperGenerator/docs/edge-cases/body-binding-edge-cases.md b/tools/WrapperGenerator/docs/edge-cases/body-binding-edge-cases.md new file mode 100644 index 00000000000..9204454b916 --- /dev/null +++ b/tools/WrapperGenerator/docs/edge-cases/body-binding-edge-cases.md @@ -0,0 +1,156 @@ +# Body-binding edge cases + +Request-body property shapes the generator classifies but does not bind. **Every shape reaching +the classifier now binds — the sweep reports 0 unbound properties across all 38 specs.** That is +a statement about the 8,164 operations (57.8% of 14,131) that generate: an operation refused +upstream — 767 suppressed because the published SDK ships no cmdlet, 5,200 unsupported shapes — +contributes no properties to any count in this file. What +remains here is one closed entry recording how the last gap was shut, and several classifications +with zero population that are retained deliberately: they exist so a future corpus change is +reported accurately instead of being silently mis-bound, and each is reported per property at +`--log-level Information` and counted by `tools/Measure-BodyPropertyCoverage.ps1`. + +The type evidence and the policies behind what is bound live in +[../body-property-binding.md](../body-property-binding.md). + +Entry template (keep field names exact so the file converts cleanly): + +``` +## +- **Class:** unsupported-shape +- **Status:** deferred | blocked | investigating +- **Counts:** occurrences / distinct (v1.0, ) +- **Evidence:** +- **Why unsafe today:** +- **Intended representation:** +- **Exit criteria:** +- **References:** +``` + +## Untyped (`UntypedNode`) — CLOSED + +- **Class:** unsupported-shape +- **Status:** closed 2026-08-13; kept as a record of how it was closed +- **Counts:** was 28 occurrences / 20 distinct (26 Files workbook internals — `maximum`, + `minimum`, `majorUnit`, `minorUnit`, `value`, `values` — and 2 + `CrossDeviceExperiences.MgUserActivity.contentInfo`). Now **0**. +- **Evidence:** the schema carries no type, reference, enum or format at all — Graph writes these + with only a `description` (`workbookChartAxis.maximum`) — and kiota emits `UntypedNode?`. + `UntypedNode` is a non-abstract base with ten subclasses; PowerShell cannot convert to the base + (`[UntypedNode]'hello'` fails), so the parameter could not be typed as the model member. +- **How it was closed:** the parameter binds as `object` and a shared `UntypedValue.From` helper + in `Shared.g.cs` converts on assignment — string to `UntypedString`, integral to + `UntypedInteger`/`UntypedLong`, fractional to `UntypedDouble`/`UntypedDecimal`/`UntypedFloat`, + bool to `UntypedBoolean`, array to `UntypedArray`, hashtable recursively to `UntypedObject`. + A `PSObject` wrapper is unwrapped first. An unrecognised CLR type throws with the type named + rather than being stringified, so an unsupported value cannot be silently sent. +- **Null handling, and how much of it is parity.** The published SDK's `AddIf` helper + (`src///generated/runtime/Extensions.cs`) adds a value only when it is + non-null **and not an empty JSON object**, and no model serializer has any explicit-null path — + so `{"prop": null}` was never sendable from this SDK and clearing a field that way was never + possible. Omitting a null and an empty hashtable is therefore parity. The *nested* rules are an + extension: AutoRest applies `AddIf` at every level it generates, including per array element + (`autorest.powershell/powershell/llcsharp/schema/array.ts:227,235`), but every AutoRest body is + a generated type, so a caller-supplied untyped bag has no published analogue and no precedent + for what a null inside it should do. Extending the same rule — dropping a nested null while its + siblings survive, dropping a null array element, omitting an object whose members all drop out — + is the wrapper's own documented contract, chosen for consistency and pinned by the runtime gate. +- **Verified:** 19 conversions runtime-tested by `tools/Test-WrapperModule.ps1` against each + module's own compiled `UntypedValue` (reached by reflection, so the gate cannot drift from a + copy of the converter), covering every branch: the seven numeric types, string, boolean, + `PSObject` unwrapping, object, array, nesting, nested-null drop, null array element drop, + empty-object omission, all-null-object omission, and the throw on an unsupported type. The + helper is emitted into every module, so the gate reports `OK(19)` for all 35 that produce a + manifest and treats a missing helper as a failure rather than N/A. Negative-tested: removing the + empty-object omission from a + module and rebuilding produced + `FAILED: empty object omitted: sent UntypedObject; all-null object omitted: sent UntypedObject`. +- **References:** issue #3707; `UntypedValue` in `CmdletEmitter.EmitSharedAuth`; + `SchemaProperties.UntypedProperty`. + +## Genuine unions + +- **Class:** unsupported-shape +- **Status:** deferred — zero population in v1.0 +- **Counts:** 0. Before the numeric/INF family was bound this shape reported 56 occurrences / + 33 distinct; every one of them was that family, so nothing remains + (`Measure-BodyPropertyCoverage.ps1`, 2026-08-12). +- **Evidence:** Graph's only union in the v1.0 corpus is a numeric with OData's `INF`/`NaN` + string alternative, which kiota resolves to the numeric and which the generator now binds. + A union whose branches are materially different schemas does not occur here — but the + classification is retained so one would be reported rather than silently mis-bound. +- **Why unsafe today:** binding one arm silently commits the caller to a type the API may not + want. Unlike the numeric family there is no branch kiota itself privileges, so there is no + evidence for which arm is right. +- **Intended representation:** most likely a parameter per arm, or a single parameter typed as + the shared base where one exists. Needs published-surface evidence before choosing. +- **Exit criteria:** the residual unions are enumerated, grouped by shape, and each group has a + kiota member type that a chosen representation demonstrably matches. +- **References:** `UnsupportedShape.Union`; `SchemaProperties.TryMapNumericUnion`. + +## Inline objects and inline enums + +- **Class:** unsupported-shape +- **Status:** deferred — zero population **among the operations that generate**, which is not + the same as zero in v1.0. +- **Counts:** 0 occurrences (`Measure-BodyPropertyCoverage.ps1`, 2026-08-12, all 38 specs). +- **Evidence:** the sweep produced no `InlineObject` or `InlineEnum` classification. For entity + CRUD bodies that is a real property of the corpus: Graph declares those objects and enums as + component `$ref`s, which is why referenced-type binding covers them. +- **Why the count is conditional:** action bodies are where Graph *does* write inline objects, + and they never reach the property classifier — an action's `requestBody` is a `$ref` to a + **requestBodies** component whose schema is an inline `type: object`, and the generator skips + the whole operation first (1,528 POSTs corpus-wide, logged as `missing supported request JSON + schema`). This population becomes non-zero the moment action generation lands. +- **Why unsafe today:** kiota synthesises a type name for an anonymous schema from its parent + and property, and that name cannot be derived from the spec alone. Guessing it is the failure + mode that produced 39 compile errors when numeric formats were first mapped. +- **Intended representation:** none required while the population is zero. The classifications + are retained deliberately so a future spec shape is reported accurately instead of being + mislabelled as something else. +- **Exit criteria:** revisit only if a corpus sweep reports a non-zero count — at which point + the kiota name must be read from a generated client before any mapping is written. +- **References:** `UnsupportedShape.InlineObject`, `UnsupportedShape.InlineEnum`. + +## Unknown string formats + +- **Class:** unsupported-shape +- **Status:** deferred — zero population in v1.0 +- **Counts:** 0 occurrences (`Measure-BodyPropertyCoverage.ps1`, 2026-08-12, all 38 specs); + every format present in the corpus is mapped. +- **Evidence:** the format inventory across all v1.0 specs is `date-time`, `int32`, `int64`, + `double`, `binary`, `base64url`, `uuid`, `time`, `date`, `duration`, `int16`, `float`, + `uint8`, `decimal` — all mapped. +- **Why unsafe today:** an unmapped format bound as `string` would compile against whatever + other CLR type kiota chose, or not compile at all. Reporting keeps the failure visible. +- **Exit criteria:** a new format appears in a sweep; its kiota member type is read from a + generated client and added to the mapping with a pinned test. +- **References:** `UnsupportedShape.UnknownFormat`; `SchemaProperties.StringFormatTypes`. + +## Dictionaries (`additionalProperties`) + +- **Class:** unsupported-shape +- **Status:** deferred — zero population in v1.0 +- **Counts:** 0 occurrences (`Measure-BodyPropertyCoverage.ps1`, 2026-08-12, all 38 specs). +- **Evidence:** no property classified as `Dictionary` in the sweep. Free-form + bags in Graph reach the caller through the `additionalData` member instead, which is excluded + by policy. +- **Why unsafe today:** untested; kiota's representation of an open map property has not been + observed in a generated client here, so any mapping would be a guess. +- **Exit criteria:** a non-zero count, then the same read-it-from-the-client procedure. +- **References:** `UnsupportedShape.Dictionary`; `ExclusionPolicy.KiotaAdditionalData`. + +## Unresolvable references and untyped arrays + +- **Class:** unsupported-shape +- **Status:** deferred — zero population in v1.0 +- **Counts:** 0 occurrences (`Measure-BodyPropertyCoverage.ps1`, 2026-08-13, all 38 specs). +- **Evidence:** three distinct situations share this classification — an array whose `items` + schema is absent, a `$ref` whose target is not in the document, and a `$ref` to a bare scalar + that has no kiota type of its own. None occurs in the corpus. +- **Why unsafe today:** unlike a schema-less property, which reliably generates as + `UntypedNode` and can therefore be bound, these have no predictable kiota member type at all. + A broken reference in particular is a spec defect; binding past it would hide the defect. +- **Exit criteria:** a non-zero count, then read the generated member type from a client and + decide per situation — they may not share one answer. +- **References:** `UnsupportedShape.Unresolvable`; `SchemaProperties.ClassifyLeaf`. diff --git a/tools/WrapperGenerator/edge-cases/crosspath-merge-edge-cases.md b/tools/WrapperGenerator/docs/edge-cases/crosspath-merge-edge-cases.md similarity index 100% rename from tools/WrapperGenerator/edge-cases/crosspath-merge-edge-cases.md rename to tools/WrapperGenerator/docs/edge-cases/crosspath-merge-edge-cases.md diff --git a/tools/WrapperGenerator/edge-cases/kiota-alignment-edge-cases.md b/tools/WrapperGenerator/docs/edge-cases/kiota-alignment-edge-cases.md similarity index 100% rename from tools/WrapperGenerator/edge-cases/kiota-alignment-edge-cases.md rename to tools/WrapperGenerator/docs/edge-cases/kiota-alignment-edge-cases.md diff --git a/tools/WrapperGenerator/edge-cases/naming-edge-cases.md b/tools/WrapperGenerator/docs/edge-cases/naming-edge-cases.md similarity index 88% rename from tools/WrapperGenerator/edge-cases/naming-edge-cases.md rename to tools/WrapperGenerator/docs/edge-cases/naming-edge-cases.md index 309c64f5e8d..f0505140b22 100644 --- a/tools/WrapperGenerator/edge-cases/naming-edge-cases.md +++ b/tools/WrapperGenerator/docs/edge-cases/naming-edge-cases.md @@ -184,6 +184,28 @@ Entry template (keep the field names exact so the file converts cleanly): - **References:** issue #3704 (remainder inventory + resolver evidence); `NamingOverrides.cs` "Collision resolutions" section. +## `-Password` / `-ForceChangePasswordNextSignIn` replaced by typed `-PasswordProfile` + +- **Class:** wrapper-surface-change +- **Status:** corrected +- **Evidence:** while body binding was primitives-only, `passwordProfile` was hard-coded into + two invented parameters so `New-MgUser` was usable at all. Neither name is published: the + shipped SDK exposes `-PasswordProfile` as a typed parameter taking a hashtable, and + `passwordProfile` is an ordinary complex property in the spec + (`anyOf[$ref microsoft.graph.passwordProfile, nullable]`), not a special case. +- **Decision:** typed binding covers every referenced-model property, so the hard-coded pair + was deleted along with the flag that emitted it. `New-MgUser -PasswordProfile @{ Password = + '...'; ForceChangePasswordNextSignIn = $true }` replaces them and matches the published + surface. +- **Migration impact:** breaking for anyone using the prototype's `-Password` / + `-ForceChangePasswordNextSignIn`. This changes only the wrapper prototype's own surface - + no published cmdlet had these parameters - and it moves toward parity rather than away. + One behaviour note: the removed code defaulted `ForceChangePasswordNextSignIn` to `true` + when only `-Password` was supplied; the typed parameter passes through exactly what the + caller sets, matching Graph's own default handling. +- **References:** issue #3707; `SchemaProperties.Classify`; `EmitsComplexPropertyAsTypedModelParameter` + in EmitterTests. + ## Watch list Cases spotted but deliberately not acted on yet, so they aren't lost: From dec945a585660c539386684de69ff4880b7aeeb7 Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Fri, 14 Aug 2026 00:41:45 -0700 Subject: [PATCH 12/13] fix(wrapper-generator): align wrapper packaging projects --- tools/Build-WrapperModule.ps1 | 109 ++++++++++-------- tools/Templates/WrapperClient.csproj.template | 17 +++ tools/Templates/WrapperModule.csproj.template | 29 +++++ 3 files changed, 108 insertions(+), 47 deletions(-) create mode 100644 tools/Templates/WrapperClient.csproj.template create mode 100644 tools/Templates/WrapperModule.csproj.template diff --git a/tools/Build-WrapperModule.ps1 b/tools/Build-WrapperModule.ps1 index 52a42169f6c..cab8c6a189c 100644 --- a/tools/Build-WrapperModule.ps1 +++ b/tools/Build-WrapperModule.ps1 @@ -8,9 +8,10 @@ For each module name, reproduces the pipeline the Mail spike proved: 1. kiota generate -> //src/Client (ApiClient + models) 2. WrapperGenerator -> //src/Cmdlets (one *.g.cs per cmdlet) - 3. write csproj -> //src/ - 4. dotnet build -> //src/bin//net10.0/ - 5. New-ModuleManifest -> .psd1 next to the dll + 3. write client project -> //src/Client/Client.csproj + 4. write wrapper project -> //src/.csproj + 5. dotnet build -> //src/bin//net10.0/ + 6. New-ModuleManifest -> .psd1 next to the dll Both generators consume the SAME OpenAPI document, so the wrappers always match the client they compile against. @@ -76,26 +77,55 @@ if (-not $SpecRoot) { $SpecRoot = Join-Path $repoRoot 'openApiDocs_KiotaCompat' if (-not $OutputRoot) { $OutputRoot = Join-Path $repoRoot 'artifacts\wrapper-modules' } $generatorProject = Join-Path $repoRoot 'tools\WrapperGenerator' $authCsproj = Join-Path $repoRoot 'src\Authentication\Authentication\Microsoft.Graph.Authentication.csproj' +$clientProjectTemplate = Join-Path $PSScriptRoot 'Templates\WrapperClient.csproj.template' +$moduleProjectTemplate = Join-Path $PSScriptRoot 'Templates\WrapperModule.csproj.template' if (-not (Get-Command kiota -ErrorAction SilentlyContinue)) { Write-Error "kiota CLI not found on PATH. Install: dotnet tool install --global Microsoft.OpenApi.Kiota" exit 1 } -# Same extraction the parity gate uses: the emitted [Cmdlet(VerbsX.Verb, "Noun")] attribute -# is the source of truth for what the dll will export, without having to load the assembly. -$cmdletAttrPattern = '\[Cmdlet\(Verbs\w+\.(\w+),\s*"((?:\\.|[^"\\])*)"' -function Get-EmittedCmdletNames { - param([string]$CmdletsDir) - Get-ChildItem -Path $CmdletsDir -Filter '*.g.cs' -File | ForEach-Object { - $match = [regex]::Match((Get-Content -Path $_.FullName -Raw), $cmdletAttrPattern) - if ($match.Success) { - "$($match.Groups[1].Value)-$([regex]::Unescape($match.Groups[2].Value))" - } +function New-ProjectFromTemplate { + param( + [Parameter(Mandatory)][string]$TemplatePath, + [Parameter(Mandatory)][string]$DestinationPath, + [Parameter(Mandatory)][hashtable]$Replacements + ) + + $content = Get-Content -Path $TemplatePath -Raw + foreach ($placeholder in $Replacements.Keys) { + $content = $content.Replace("{$placeholder}", $Replacements[$placeholder]) + } + $unresolved = [regex]::Matches($content, '\{[A-Za-z][A-Za-z0-9]*\}') | ForEach-Object Value | Sort-Object -Unique + if ($unresolved) { + throw "unresolved placeholder(s) in $TemplatePath`: $($unresolved -join ', ')" + } + Set-Content -Path $DestinationPath -Value $content -Encoding utf8 +} + +function Get-CompiledCmdletNames { + param([Parameter(Mandatory)][string]$AssemblyPath) + + # Import in a child process so discovery observes the compiled binary PowerShell will load, + # and so assemblies from one module cannot contaminate or lock the next module's build. + $escapedAssemblyPath = $AssemblyPath.Replace("'", "''") + $discovery = @" +`$ErrorActionPreference = 'Stop' +`$module = Import-Module -Name '$escapedAssemblyPath' -PassThru +[pscustomobject]@{ Cmdlets = @(`$module.ExportedCmdlets.Keys | Sort-Object) } | + ConvertTo-Json -Compress +"@ + $encodedDiscovery = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($discovery)) + $output = & pwsh -NoProfile -NonInteractive -EncodedCommand $encodedDiscovery 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "compiled module discovery failed: $(($output | Select-Object -Last 3) -join ' | ')" } + $json = $output | Where-Object { $_ -match '^\{' } | Select-Object -Last 1 + if (-not $json) { throw 'compiled module discovery produced no result' } + @((ConvertFrom-Json $json).Cmdlets) } -function Build-OneModule { +function Build-Module { param([string]$Name) $started = Get-Date @@ -157,38 +187,22 @@ function Build-OneModule { return $result } - # Relative to $srcDir rather than the absolute $authCsproj, so the csproj is portable - # across clones and stays correct if a module's output folder ever moves (the eventual - # src/// commit target sits at a different depth than - # artifacts/wrapper-modules//src/). + $clientAssemblyName = "$moduleName.Client" + $clientCsprojPath = Join-Path $clientDir 'Client.csproj' + New-ProjectFromTemplate -TemplatePath $clientProjectTemplate -DestinationPath $clientCsprojPath -Replacements @{ + ClientAssemblyName = $clientAssemblyName + } + + # Project references are relative so generated projects remain portable across clones + # and across the artifacts and eventual src///wrapper layouts. $csprojPath = Join-Path $srcDir "$moduleName.csproj" $authCsprojRelative = [System.IO.Path]::GetRelativePath($srcDir, $authCsproj) -replace '/', '\' - @" - - - - - net10.0 - latest - enable - enable - $moduleName - - true - `$(NoWarn);CS1591 - - - - - - - - - - - - -"@ | Set-Content -Path $csprojPath -Encoding utf8 + $clientCsprojRelative = [System.IO.Path]::GetRelativePath($srcDir, $clientCsprojPath) -replace '/', '\' + New-ProjectFromTemplate -TemplatePath $moduleProjectTemplate -DestinationPath $csprojPath -Replacements @{ + ModuleAssemblyName = $moduleName + ClientProjectPath = $clientCsprojRelative + AuthenticationProjectPath = $authCsprojRelative + } $buildOut = & dotnet build $csprojPath -c $Configuration --nologo -v minimal 2>&1 if ($LASTEXITCODE -ne 0) { @@ -197,10 +211,11 @@ function Build-OneModule { return $result } - $cmdlets = @(Get-EmittedCmdletNames -CmdletsDir $cmdletsDir) + $binDir = Join-Path $srcDir "bin\$Configuration\net10.0" + $assemblyPath = Join-Path $binDir "$moduleName.dll" + $cmdlets = @(Get-CompiledCmdletNames -AssemblyPath $assemblyPath) if ($cmdlets.Count -eq 0) { $result.FailedAt = 'manifest'; $result.Error = 'no cmdlets emitted'; return $result } - $binDir = Join-Path $srcDir "bin\$Configuration\net10.0" $psd1Path = Join-Path $binDir "$moduleName.psd1" New-ModuleManifest -Path $psd1Path ` -RootModule "$moduleName.dll" ` @@ -227,7 +242,7 @@ function Build-OneModule { $results = foreach ($name in $Module) { Write-Host "=== $name ===" -ForegroundColor Cyan - $r = Build-OneModule -Name $name + $r = Build-Module -Name $name if ($r.Status -eq 'OK') { Write-Host " OK: $($r.CmdletCount) cmdlets -> $($r.Psd1) ($($r.Seconds)s)" -ForegroundColor Green } diff --git a/tools/Templates/WrapperClient.csproj.template b/tools/Templates/WrapperClient.csproj.template new file mode 100644 index 00000000000..def417080fd --- /dev/null +++ b/tools/Templates/WrapperClient.csproj.template @@ -0,0 +1,17 @@ + + + + + net10.0 + latest + enable + enable + {ClientAssemblyName} + $(NoWarn);CS1591 + + + + + + + \ No newline at end of file diff --git a/tools/Templates/WrapperModule.csproj.template b/tools/Templates/WrapperModule.csproj.template new file mode 100644 index 00000000000..aad53206308 --- /dev/null +++ b/tools/Templates/WrapperModule.csproj.template @@ -0,0 +1,29 @@ + + + + + net10.0 + latest + enable + enable + false + {ModuleAssemblyName} + + true + $(NoWarn);CS1591 + + + + + + + + + + + + + + + + \ No newline at end of file From 00ac182a0983516e0173d73deabb8222c344c11e Mon Sep 17 00:00:00 2001 From: Joywambui-maina Date: Thu, 20 Aug 2026 21:14:08 -0700 Subject: [PATCH 13/13] fix(wrapper-generator): address Copilot review on request-body binding A bare primitive-typed branch (anyOf[$ref, {type: string}]) no longer reads as a nullability placeholder, so such a union is reported instead of silently collapsing to the reference - pinned by a test; generation output across the current specs is byte-identical. Also fixes a comment typo in NamingTests. --- tools/WrapperGenerator.Tests/NamingTests.cs | 2 +- .../SchemaPropertiesTests.cs | 22 +++++++++++++++++++ tools/WrapperGenerator/SchemaProperties.cs | 7 +++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tools/WrapperGenerator.Tests/NamingTests.cs b/tools/WrapperGenerator.Tests/NamingTests.cs index a065649b851..843d1c7cfed 100644 --- a/tools/WrapperGenerator.Tests/NamingTests.cs +++ b/tools/WrapperGenerator.Tests/NamingTests.cs @@ -140,7 +140,7 @@ public void ResolvesPublishedSdkNames(string method, string path, string expecte [Theory] // Deliberate corrections: the published name is wrong (an AutoRest naming defect) and the // generator emits the corrected name instead of reproducing it. Every entry here must have - // an docs/edge-cases/naming-edge-cases.md entry and a matching row in + // a docs/edge-cases/naming-edge-cases.md entry and a matching row in // Compare-WrapperCmdletNames.ps1's $deliberateCorrections table, so the parity gate // reports it as [CORRECTED], not a failure. // Shipped: Get-MgSecurityThreatIntelligenceHostWhoi — the only whois-family cmdlet (of 30) diff --git a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs index 5adc167c64d..c405997a20d 100644 --- a/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs +++ b/tools/WrapperGenerator.Tests/SchemaPropertiesTests.cs @@ -159,6 +159,28 @@ public void BindsNullableReferenceComposition() Assert.False(complex.IsArray); } + // A primitive-typed branch is a real union arm, not a nullability placeholder: + // anyOf[$ref, {type: string}] must not collapse to the reference and silently drop the + // string alternative. + [Fact] + public void ReportsUnionOfAModelAndAPlainString() + { + var classified = ClassifyBody(new Dictionary + { + ["identity"] = new OpenApiSchema + { + AnyOf = + [ + new OpenApiSchemaReference("graph.identity"), + new OpenApiSchema { Type = JsonSchemaType.String }, + ], + }, + }); + + Assert.Empty(classified.Complex); + Assert.Equal(UnsupportedShape.Union, Assert.Single(classified.Unsupported).Shape); + } + [Fact] public void BindsDirectReferenceAndReferenceArray() { diff --git a/tools/WrapperGenerator/SchemaProperties.cs b/tools/WrapperGenerator/SchemaProperties.cs index af6f9146955..cae86406836 100644 --- a/tools/WrapperGenerator/SchemaProperties.cs +++ b/tools/WrapperGenerator/SchemaProperties.cs @@ -353,7 +353,12 @@ private static IEnumerable EnumValues(IOpenApiSchema schema) // A branch that adds nullability and nothing else: no reference, no members, no enum, no // items, no format. private static bool IsNullabilityPlaceholder(IOpenApiSchema schema) => - (schema.Properties?.Count ?? 0) == 0 + // A primitive-typed branch is a real union arm, not a nullability marker: collapsing + // anyOf[$ref, {type: string}] to the reference would silently drop the string + // alternative. The docs spell the placeholder `{ type: object, nullable: true }`, so + // only an untyped or Object-typed branch (Null bit masked, as everywhere else) qualifies. + ((schema.Type ?? JsonSchemaType.Null) & ~JsonSchemaType.Null) is 0 or JsonSchemaType.Object + && (schema.Properties?.Count ?? 0) == 0 && (schema.Enum?.Count ?? 0) == 0 && schema.Items is null && schema.AdditionalProperties is null