Skip to content

Master Implementation PR: generic templates for $dynamicRef bindings across languages - #8115

Draft
Abdullah Alaqeel (aqeelat) wants to merge 16 commits into
microsoft:mainfrom
aqeelat:feat/dynamicref-csharp-generics
Draft

Master Implementation PR: generic templates for $dynamicRef bindings across languages#8115
Abdullah Alaqeel (aqeelat) wants to merge 16 commits into
microsoft:mainfrom
aqeelat:feat/dynamicref-csharp-generics

Conversation

@aqeelat

Copy link
Copy Markdown
Contributor

Master Implementation PR

This is the master implementation PR for multi-language generic template support of $dynamicRef bindings (Phase 4b of #7815). It tracks the complete implementation; we will split this into multiple smaller PRs before final review.

What's here

  • Shared builder machinery: generic binding frames, class promotion inside the build lifecycle, base-class closing (Derived<T> : Base<T>), shared nested inline models, concrete-path fallbacks for unsupported shapes (discriminators, recursive bound anchors, composed anchor targets), and deterministic bare/bound resolution
  • Per-language generic emission: C#, Java, Dart (constructor-injected ParsableFactory<T>), Python (__class_getitem__ subscription binding), TypeScript (generic interfaces + factory functions closed by partial application), Go (generic structs + able-interfaces)
  • PHP and Ruby intentionally stay on per-binding concrete classes (no generics in either language)
  • Extensive builder/writer/integration tests plus compile checks against the real abstractions libraries (C#, Java, Go, TypeScript)

Planned split

Roughly one PR per language on top of a shared-contract PR, following the commit boundaries already in this branch.

Review on individual units will happen in the split PRs; this PR exists to make the overall direction and progress visible. The Phase 4b checkbox in #7815 stays open until the split PRs merge.

C# $ref-bound dynamic-ref templates now emit one reusable generic class
(PaginatedTemplate<TItemType>) instead of per-binding concrete classes
(PaginatedTemplateUser). Anchor-typed properties become type parameters;
executor/request-body usages close over the bound types via the existing
generic-argument channel, with constructor-injected ParsableFactory fields
driving deserialization inside the template. Recursive '#self' anchors
close over the type parameters.

Scope gates (documented in UseGenericDynamicBindingTemplates): only C#;
all anchors $ref-bound; additionalProperties dynamic refs and allOf
templates stay on the Phase 2 concrete path. Unbound references to a
generic template degrade to UntypedNode with a warning.
… nested inline models

Inherited templates now reuse the generic binding frame: InheritedTemplate<TItemType>
keeps a concrete PageBase base, while BasePage promotes to BasePage<TItemType> when
it resolves items through the active binding and DerivedPage<TItemType> closes over
the base argument, forwarding itemTypeFactory to it. Derived declarations own fresh
type parameter instances so base and derived never share a CodeDOM element.

Generic promotion moved into the class build lifecycle (single-threaded per class,
lifecycle registered before publication) so parallel bindings observe final
genericity; bare or ordering-conflicted references degrade to UntypedNode instead
of emitting open generic types.

Nested inline models containing $dynamicRef share the enclosing template's type
parameters (InlineModel<TItemType>), staying open inside the template and closing
only at usage sites. The C# declaration writer now emits factory fields only for
parameters the class's own deserializers consume.
… templates

- language gate becomes GenericTemplateLanguages (CSharp only; per-language
  entries pre-stubbed commented for the Dart/Java/Python/TypeScript/Go ports)
- CodeFunction gains TypeParameters/IsGeneric (TypeScript generic factory fns)
- CommonLanguageRefiner: import generation skips CodeTypeParameter definitions;
  CopyClassAsInterface carries type parameters and closes parent interface
  implements over the interface's own parameters
- pin PHP/Ruby concrete per-binding fallback with parameterized tests (no
  generics in either language, permanent concrete path)
Render generic model declarations (class X<TItemType extends Parsable>) with
constructor-injected ParsableFactory<T> fields owned only by the parameters
each class's own deserializers consume, forwarding factories to generic bases.
Deserializer/executor sites resolve CodeTypeParameter properties through the
injected factory and close generic classes with closures at usage sites.
GetTypeString now renders generic type arguments and CodeTypeParameter names.
Generic models declare <TItemType extends Parsable> with a synthesized
constructor injecting ParsableFactory fields (own-deserializer parameters
only) and super-forwarding for generic bases. Deserializers and executor
send calls use the injected factory fields and instantiating lambdas.
…cription binding

Generic models declare TypeVar + Generic[TItemType] bases with a cached
__class_getitem__ that binds _item_type on a dynamic subclass, so existing
class-object factory call sites work unchanged (PaginatedTemplate[User]).
The factory becomes a classmethod returning cls(); parameter-typed
properties deserialize through type(self)._item_type.
…ons for dynamicRef bindings

Generic models convert to generic interfaces (interface X<TItemType>) whose
factory and serializer functions take the item factory/serializer as leading
parameters; executor metadata closes the generics with partial application
(createXFromDiscriminatorValue(createUserFromDiscriminatorValue)) since the
runtime constructs object literals without constructors.
Generic models declare type X[TItemType serialization.Parsable] structs with
unexported ParsableFactory fields set by generic constructors; able-interfaces
carry the type parameters and closed usages instantiate with them
(NewPaginatedTemplate[Userable](CreateUserFromDiscriminatorValue)). Nullable
parameter-typed properties no longer render with a pointer star.
Open GenericTemplateLanguages to Java, Dart, Python, TypeScript and Go with
per-language integration expectations for the generic fixtures; PHP and Ruby
remain on the permanent concrete per-binding path. Extend ReadGeneratedModelText
to Dart/PHP/Ruby outputs.
… self-references

IsSelfReferenceToEnclosingTemplate compared two null reference ids as equal,
so when the template itself is inline (operation-local with a $defs binding),
every nested inline object property was forwarded to the template instead of
materializing its own model (metadata: MyResponse<TItemType>). Require a
non-empty reference id before comparing.
AllTypes does not expand GenericTypeParameterValues, so request builders and
models referencing PaginatedTemplate<User> never imported user.dart and the
generated code did not compile. Mirror the Java generic-argument using pass,
keeping same-namespace arguments since Dart imports sibling model files.
…rete path

- the bare-reference pre-scan now traverses $ref targets, so a bare template
  reference hiding behind a wrapper $ref hop is recorded and the template
  deterministically stays concrete instead of degrading order-dependently
- templates carrying discriminators are excluded from generics (their
  polymorphism relies on factory methods generic classes skip)
- bindings whose anchor-bound schema itself contains a dynamic reference are
  excluded (the bound class would be promoted to generic, emitting open
  generics at usage sites); the Phase 2 suffixed concrete classes cover them
- drop stale agent TODO comments from GenericTemplateLanguages
…odel builds

WaitForGenericModelParameters blocked on another class's CountdownEvent for
every resolved class. Property-type resolution runs while the current thread
holds the enclosing class's lifecycle monitor, so two parallel threads
building mutually-referencing models (A.b -> B, B.a -> A) could each wait
forever on the other. Track a thread-local class build depth and skip the
wait while it is positive — returning the mid-build class is the pre-existing
tolerated behavior for circular references — and skip entirely for languages
that never promote generic models.
…or resolves to a composed type

An anchor bound via $ref to a oneOf/anyOf component resolves to a union type
which cannot serve as a generic argument; the generic closing would then emit
usages with fewer type arguments than declared (CS0305 and equivalent).
When any anchor fails to resolve to a single type, restart the binding frame
as concrete (suffixed) before the class is created, keeping full union typing
in the per-binding class instead of degrading the endpoint.
… the bare-reference pre-scan memo

Error-mapping types now expand GenericTypeParameterValues like return types
so argument classes of generic error templates are not trimmed away. The
bare-reference pre-scan keeps one shared (schema, scope) memo across
operations instead of a per-operation visited set, so large specs do not
re-walk shared component graphs per operation; a schema first walked in
binding scope is still re-walked when later reached bare.
…eneric ceilings

AddGenericTypeArgumentsImports moves to CommonLanguageRefiner with a
keepSameNamespaceArguments flag (Java false, Dart true). Extend the generic
ceilings note: the bare-reference pre-scan ordering limitation and generic
models dropping constructor default values.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant