feat: resolve $dynamicRef generic bindings with per-context classes#7978
feat: resolve $dynamicRef generic bindings with per-context classes#7978aqeelat wants to merge 2 commits into
Conversation
5a95ec9 to
2dff270
Compare
2dff270 to
7465ee3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:548
- These assertions only guard against an unsuffixed
TreeTemplatein languages that emit theclasskeyword. For Go, an unsuffixed template would be emitted astype TreeTemplate struct, which isn’t currently checked.
Assert.DoesNotContain("class TreeTemplate\n", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class TreeTemplate ", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class TreeTemplate:", allModelText, StringComparison.Ordinal);
tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:575
- These assertions only check for
class SearchTemplate..., which won’t catch an unsuffixed Go declaration (type SearchTemplate struct). Adding the Go-specific pattern makes the per-language guarantee more robust.
Assert.DoesNotContain("class SearchTemplate\n", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class SearchTemplate ", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class SearchTemplate:", allModelText, StringComparison.Ordinal);
tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:603
- Same issue as other template checks:
class EnvelopeTemplate...won’t detect an unsuffixed Go model (type EnvelopeTemplate struct).
Assert.DoesNotContain("class EnvelopeTemplate\n", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class EnvelopeTemplate ", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class EnvelopeTemplate:", allModelText, StringComparison.Ordinal);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
tests/Kiota.Builder.Tests/KiotaBuilderDynamicRefTests.cs:16
KiotaBuilderis referenced without importing theKiota.Buildernamespace, so this new test file won’t compile (unless a global using exists, which it doesn’t in this test project). Addusing Kiota.Builder;or fully-qualify the type.
public void ExtractAnchorNameReturnsExpectedValue(string dynamicRef, string expected)
{
Assert.Equal(expected, KiotaBuilder.ExtractAnchorName(dynamicRef));
}
a15bbd1 to
bbc5397
Compare
…classes test(builder): expand dynamic-ref coverage and fix review items
bbc5397 to
354fc0d
Compare
354fc0d to
6772aaf
Compare
| var allText = ReadGeneratedModelText(Path.Combine(Directory.GetCurrentDirectory(), "Generated", "ArrayRootDynamicRef", language.ToString())); | ||
| Assert.DoesNotContain("UntypedNode", allText, StringComparison.Ordinal); | ||
| Assert.Contains("User", allText, StringComparison.Ordinal); | ||
| Assert.Contains("Group", allText, StringComparison.Ordinal); | ||
| } |
| // Mixed anchors: one $ref (zRef → User) + one inline (aInline). | ||
| // Ref suffixes come first (sorted), context appended once after. |
| // If typeNameForInlineSchema is not null and the schema is referenced, we have most likely unwrapped a referenced schema(most likely from an AllOf/OneOf/AnyOf). | ||
| // Therefore the current type/schema is not really inlined, so invalidate the typeNameForInlineSchema and just work with the information from the schema reference. | ||
| if (schema.IsReferencedSchema() && !string.IsNullOrEmpty(typeNameForInlineSchema)) | ||
| // Unresolved $dynamicRef is the exception: its active binding can resolve to an inline schema, so keep the caller-provided inline name. |
baywet
left a comment
There was a problem hiding this comment.
Thanks for the contribution!
Thanks for iterating with copilot here.
Beyond the comment I left, and the last few from copilot, I think we're getting really close!
| if (!string.IsNullOrEmpty(operation?.OperationId)) | ||
| suffix += operation.OperationId.CleanupSymbolName().ToFirstCharacterUpperCase(); | ||
| if (isRequestBody) | ||
| suffix += "RequestBody"; |
There was a problem hiding this comment.
can we move those to consts if we haven't already defined them? and replace other usage points?
There was a problem hiding this comment.
Done. RequestBodySuffix and ResponseSuffix are constants alongside the existing field-level constants, and the corresponding inline literals now use them.
|
(we'll also need a changelog entry) |
|
@baywet can you ask copilot to do a thorough review? It might speed up the process to get all feedback at once. |
|
@aqeelat not through the UI at least. Maybe tweaking things like the agents.md or the copilot instructions though. But I suspect they designed the feature this way to avoid overwhelming contributors. In my experience most people don't like to get more than 10 or so comments at once. And there's an aspect of importance first (no point telling you about typos in comments if you're going to need to revamp the method anyway). I'll set up a workflow about the changelog entry requirement though, I've typed that so many times over the years I can't imagine how it's not automated yet. |
6772aaf to
1668b2f
Compare
Summary
Phase 2 of #7815. Builds on Phase 1 (#7817) which resolved recursive
$dynamicRefvia a thread-local dynamic-scope stack.This PR adds support for the template binding pattern: when the same template schema is referenced from multiple contexts with different
$defs+$dynamicAnchorbindings, each binding context now produces a distinct concrete class instead of silently sharing one class with the first binding's types. This PR emits concrete specializations (e.g.,PaginatedTemplateUser,PaginatedTemplateGroup), not reusable generic type declarations.What changed
KiotaBuilder.cs:DynamicScopeFrame(Schema, BindingSuffix)record replaces bareIOpenApiSchemaon the dynamic-scope stack, carrying the computed suffix alongside each frame.TryGetDynamicBindingSuffix— when a schema carries$defsentries with$dynamicAnchor, produces a suffix from the bound type names (or route segment + operation context for inline bindings). Definitions are sorted by key for deterministic output. Context suffix is computed once, not duplicated per anchor.ContainsDynamicReference+GetActiveDynamicBindingSuffix— when a component has no local$defsbut contains a reachable$dynamicRef, inherits the binding suffix from the active dynamic scope.$defs), then the unwrapped target. Recursive check runs beforeGetExistingDeclarationso a bare class can't bypass the suffix.CreateCollectionModelDeclarationconditionally pushes onto the dynamic scope when the array schema carries$defswith$dynamicAnchor.ExtractAnchorNamechanged fromprivatetointernalfor unit testing.Test coverage
generic-binding.yaml$refbindings in$defsinline-binding.yaml$ref)recursive-generic-binding.yamlrequest-body-generic-binding.yamlmulti-anchor-generic-binding.yaml$dynamicAnchorslots per templatearray-root-dynamicref.yaml$dynamicRefin array items at response rootunresolved-dynamicref.yaml$dynamicRef→UntypedNodemulti-inline-binding.yamlmixed-anchor-binding.yaml$ref+ inline anchors, orderinginherited-generic-binding.yamlmulti-error-inline-binding.yamlrequest-response-inline-binding.yamlinherited-component-binding.yamlno-operation-id-inline-binding.yamlnamespaced-binding.yamlv1.Uservsv2.User)Five-language fixtures assert language-specific typed deserialization. C#-only fixtures assert builder-level model identity and binding-specific type resolution.
ExtractAnchorNameunit tests added inKiotaBuilderDynamicRefTests.cs.Behavioral change
Referenced schemas whose site declares
$defsentries containing$dynamicAnchor+$refnow get a suffixed class name. This is the intended fix but will rename classes for any existing spec that happens to match this pattern.Remaining limitations (Phase 3+)
$dynamicRefwith no active binding context still degrades toUntypedNodewith a warning.Template<T>) are not emitted; each binding produces a concrete class.