Skip to content

feat: the binder bridges a collection of collections - #167

Merged
ShocOne merged 1 commit into
mainfrom
feat/the-binder-bridges-a-collection-of-collections
Aug 30, 2026
Merged

ShocOne merged 1 commit into
mainfrom
feat/the-binder-bridges-a-collection-of-collections

Conversation

@ShocOne

@ShocOne ShocOne commented Aug 30, 2026

Copy link
Copy Markdown
Member

Cut from main after #166. Second of the three nested-collection PRs; the emit + fixtures + curated-fixture PR follows once this merges.

What

The SDK binding layer and the conversion catalog carry list of lists, list of maps, map of lists and map of maps — and deeper, with a scalar at the bottom — under both dialects. Nothing generates differently yet: derivation still excludes depth ≥ 2 under nestedCollectionElement until the emit PR removes that gate, so this lands the machinery with its tests and changes no generated tree.

Binder (internal/sdkbind)

  • FieldBinding.NestedCollectionElementTypes copies the derivation's levels.
  • Both binders draft one plain SDK type composed to depth (nestedCollectionGoType: [][]string, map[string]map[string]int64) and one conversion pair, FromNestedCollectionSlice/Map / ToNestedCollectionSlice/Map.
  • prune_collections.go: settleCollection settles the draft against the carrier the SDK actually has, in order:
    1. kiota untyped node (serialization.UntypedNodeable, matched on package path like isKiotaDateOnly) → …SliceUntypedNode; a map declared over one is excluded.
    2. one bag model (GetAdditionalData) for a map of lists / map of maps → …MapAdditionalData, model + write constructor carried (the existing scalar bag arm now shares settleBag).
    3. slice of bag models for a list of maps → …SliceAdditionalData.
    4. plain Go value matched level by level (plainCollectionMatches: slice ↔ list, string-keyed map ↔ map, compatible basic leaf; a pointer at any level, a named leaf or a non-string key does not match) → …Slice/Map.
    5. otherwise unbridgeableType, the reason naming the declared shape ("does not nest the way the document declares a map of list of string").
  • nestedModelOf unwraps every collection level, so an object at the bottom of a nested collection resolves against its model.

Conversion catalog (templates)

  • Shared: APIToFrameworkNestedCollectionList/Map(ctx, values any, elementType attr.Type) and FrameworkToAPINestedCollectionList/Map[T any](ctx, value, setter func(T)) — the framework's own reflection walks nested Go values; nil at any depth is null, empty stays empty.
  • nested_collections_kiota.go.tmpl: …MapAdditionalData, …ListAdditionalData[T] (one model per element, built through a construct func() T), …ListUntypedNode; walkers frameworkValueFromBag / bagValueFromFramework / anyFromUntypedNode / untypedNodeFromAny. Rules: a parsed bag holds []any, map[string]any and pointer scalars with every number a float64; a stray map entry is skipped alone (as test: the curated fixture carries a kiota additionalData bag #162 decided), a stray list element becomes the typed null so positions hold; nil is the typed null at every level.
  • bagString / bagBool extracted so the four scalar bag helpers and the walkers share one leaf rule.

SDK shapes this was verified against

Generated from a probe spec with kiota 1.34.1 and openapi-generator (scratchpad, not committed): openapi-generator emits [][]string, []map[string]string, map[string][]string, map[string]map[string]string; kiota emits UntypedNodeable for a list of lists, []Thing_listOfMapsable for a list of maps, and a single Thing_mapOfLists/Thing_mapOfMaps bag model for the map forms. GetRawValue in kiota-serialization-json-go@v1.1.4 unwraps nested arrays/objects recursively; WriteAdditionalData marshals []any/map[string]any through json.Marshal.

Tests

  • prune_collections_test.go: every pairing at two levels and one at three through the plain arm; seven shapes that must not match (short, deep, map-for-list, pointer, non-string key, named leaf, incompatible leaf); map of lists through a bag; list of bags with the constructor; list of lists through the untyped node (and a foreign UntypedNodeable not matching); nestedModelOf to depth; nestedCollectionGoType / describeCollection.
  • Binder drafting tests in both dialect test files.
  • Template tests run inside both generated curated trees by TestUnit_Run_CuratedTreeCompiles: plain round trips for all four shapes, nil/empty/null/unknown rules; kiota bag forms, stray-entry vs stray-element rules, bag round trip, one-model-per-element, untyped-node round trip and a scalar node reading as null.

Verification

make check green: sdkbind 83.3% (from 82.6%), 90.2% total, hygiene gate clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XPLePCqzyYncF7TedagFkg

The SDK binding layer and the conversion catalog carry a list of lists, a
list of maps, a map of lists and a map of maps under both dialects, to
any depth with a scalar at the bottom.

The binder copies the derivation's levels onto the field binding and
drafts one plain SDK type composed to depth — [][]string,
map[string][]int64 — with one conversion pair per direction,
FromNestedCollectionSlice/Map and ToNestedCollectionSlice/Map, that takes
the whole value and the framework element type. Pruning settles that
draft against whichever of four carriers the SDK actually has: a plain Go
value, matched level by level so the framework's reflection can never
meet a shape the binder did not approve; one kiota model whose only
content is an additionalData bag, which is how kiota carries a map of
lists or of maps; a slice of such models, which is how it carries a list
of maps; and kiota's untyped node, matched on its package path, which is
how it carries a list of lists. An SDK type that does not nest as the
document declares is excluded naming the declared shape. nestedModelOf
unwraps every collection level, so an object at the bottom resolves
against its model.

The catalog gains the helpers those shorthands name. The plain pair
hands the value to types.ListValueFrom / ElementsAs, whose reflection
already walks nested Go values. The kiota pairs walk a bag against the
schema's element type: a parsed response carries arrays as []any,
objects as map[string]any and scalars as pointers, every number a
float64; a map entry of the wrong shape is skipped alone, and a list
element of the wrong shape becomes the typed null so the elements after
it keep their positions. The untyped-node pair unwraps kiota's node kinds
to the same plain values and wraps them again on the way out. bagString
and bagBool join bagInt64 and bagFloat64 as the one leaf rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPLePCqzyYncF7TedagFkg
@ShocOne
ShocOne merged commit c9b00df into main Aug 30, 2026
2 checks passed
@ShocOne
ShocOne deleted the feat/the-binder-bridges-a-collection-of-collections branch August 30, 2026 09:26
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