feat: Kiota SDK generation, the kiotaFluent dialect, and embed/external SDK modes - #105
Merged
Conversation
…embedded or external A new sdk noun group drives Microsoft Kiota as a gated PATH tool: sdk generate reads a pinned, checksum-verified OpenAPI snapshot and generates a Go SDK from it, so a provider can bind to an SDK derived from the same document its blueprints were drafted from. kiota's own kiota-lock.json is the committed source of truth; the version gate refuses a binary that does not match it, naming both versions, because a silent kiota bump rewrites the whole tree as an inexplicable diff. Output passes through the same gofumpt form the provider generator uses. -mode embed (the default) generates under the enclosing module -- kiota's intended layout, with the import path derived from the nearest go.mod plus the relative output dir -- so bindings check, drift checks and provider push all see one module. -mode external emits a standalone tree with a minimal generated go.mod resolved by go mod tidy, ready for its own repository. -check regenerates into scratch and byte-compares, catching hand edits, snapshot drift and version drift alike; -dry-run prints the resolved snapshot and invocation. Unit tests drive the full surface against a deterministic stub kiota on PATH (embed module derivation, the version gate naming both versions, check catching a hand edit and passing after regeneration, external go.mod emission, dry-run inertness). docs/cli.md documents the group and the drift test holds it there; the retired-spelling ban narrows from '-mode ' to 'probe -mode' now that a legitimate -mode flag exists again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… assertion
The IR can now express a request-builder chain as data: Operation.Chain is
a list of ChainSegment{method, args}, the final segment the verb, a
mid-chain identifier an ordinary stateField argument -- the same call-is-
data doctrine the flat method style follows, so the emitter will render a
chain generically without knowing which generator produced the SDK.
ConvertCall gains ExtraArgs for helpers needing a companion function (a
generated SDK's enum parse function), and NestedAttributeObject gains
ConstructorExpr for setter-based element construction. All additive; every
existing blueprint is byte-identical and the kiotaFluent/CallStyleFluent/
AccessMethod refusals stay in force -- but the chain's structural rules
(chain required under fluent, no chain under method, verb from the last
segment, no transport arity) already validate, so a malformed chain is
named precisely the moment the refusals lift.
provider.sdk gains mode (embed|external; absent means external, which is
what every existing blueprint always meant) and generator. Validation
makes modulePath finally load-bearing: embed requires it to equal
provider.goModule, an external declaration of the provider's own module is
named as really-embed, and every binding's service import must live under
the declared module. The postcheck battery gains an offline first line --
AssertGoMod holds the hand-owned go.mod to the blueprint's declared SDK:
an external SDK must be required at source.sdkVersion and an embedded
kiota SDK needs its runtime module, with the failure naming the exact
go get. Verified against the live pilot: the committed go.mod passes at
v0.2.0 and the whole-tree drift check stays green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…view builder readExpr, writeStmt and expandStmt in access.go are now the only place the emitter knows how an SDK model's fields are reached: struct assignment for the resty dialect, Get/Set method pairs for a setter-based one. The eight sites that previously spelled body.X = / remote.X inline -- construct expand, state flatten, nested expand and flatten, ephemeral state, list identity and display -- all route through the seam, with the access style threaded from each binding's declared AccessStyle via the schema scope into every nested shape. The zero style means struct fields, so every existing blueprint and fixture renders byte-identically: proven by the untouched golden tests and by provider generate -check staying green against the committed pilot. A setter cannot receive a two-value call, so under method access every fallible conversion lands in a temp -- the shape table tests pin all five statement forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… facts The type checker gains the method-shaped half it needs for a fluent SDK. MethodChain walks a request-builder chain from the client type -- every hop resolved on the real method set, argument counts held to the signatures, a builder hop demanded to return exactly one value -- and hands the final verb to the same arity and result-type contract a flat call gets, extracted as verifyMethodContract. LookupAccessorPair verifies a model's Get/Set pair and is where a mangling miss (GetError vs GetErrorEscaped) surfaces with the real spelling; LookupFieldAccess dispatches on the binding's declared AccessStyle, the checker-side twin of the emitter's access seam. Wire-field checks demand setters on write targets and getters alone on read models -- builders return read-side interfaces whose method sets legitimately carry no setters. A bare '<receiver>.client' accessor is now the legal fluent root, and refused with a message when the binding declares a service type it never reaches. Static facts are dialect-gated with a stated reason: a method-access SDK is pointer-typed behind accessors with no encoder-level omitempty, so the zero-value-unsendable class cannot arise -- an empty document, not an error, keeping the -check drift gate uniform across dialects. A miniature kiota-shaped SDK module under testdata (fluent client, typed indexer, interface-returning verbs, keyword-mangled accessor) exercises every new path, including the two-result builder refusal and the did-you-mean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…taFluent refusals The reserved dialect is now real end to end in the emitter. opView renders a fluent operation through chainCall -- the chain is the call, as data, with each segment's arguments rendered by the enclosing scope's own rules -- and the acceptance test helper rebases the same chain onto its bare client via the argument renderer it already had. Fluent results carry a NilResultGuard the templates act on where nil is meaningful: a read treats an empty success body exactly like a not-found, a create refuses to record an object it cannot identify (errors.ErrEmptyResponse, defined by the kiota errors scaffold), a data source and an ephemeral report it as the missing object it is, and a list treats it as an empty collection. Struct-field SDKs never set the flag, so resty output is untouched -- provider generate -check stays green against the committed pilot. convertExpr renders ConvertCall.ExtraArgs, completing the enum-parse companion plumbing. All the refusals lift together, as one coherent capability: the dialect, CallStyleFluent, and the three AccessMethod refusals become plain enum membership checks; a service typeName is required only where a method-style call actually uses one; and a new coherence pass refuses a fluent operation under a non-fluent dialect by name -- the refusal tests now assert that rule and the chainless-fluent emitter guard instead of the old reservations. A fixture that rebinds the committed tag resource onto a kiota-shaped SDK proves the whole surface: chain calls, setter construction, getter state mapping, getter ID assignment, guard placement, and the rebased helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k-dialect
Inference can now draft the fluent dialect. blueprint draft grows
-sdk-dialect restyService|kiotaFluent and -sdk-models-package; under kiota
the resty -sdk-service-root/-sdk-accessor knobs are refused by name if
typed. The drafted bindings are chains derived from each operation's path
template -- /tags/{tagId} becomes Tags().ByTagId(id).Get(ctx, nil), one
builder hop per literal segment, the identifier riding the typed indexer,
nil request configuration until somebody curates one -- with method access,
model constructors, interface response types (<Type>able) and
created.GetId() for the identifier.
Naming is derived from the OpenAPI document, deliberately not by parsing
the generated SDK: kiotaName reproduces kiota's plain word-capitalisation
(accountGroupId -> AccountGroupId, never AccountGroupID) and the keyword
mangling (error -> GetErrorEscaped), preserving the draft-then-curate
order; a wrong guess surfaces in bindings check with the true spelling.
Field gains the schema's format string, and conversionsFor reads it the
way kiota does -- a plain integer is *int32, only format int64 widens,
float narrows -- while the resty rules are untouched. A kiota enum wires
through convert.KiotaEnumToFramework and FrameworkToKiotaEnum with the
ParseX companion in ExtraArgs, so a value the validator missed becomes a
diagnostic rather than a silent nil. Proven end to end by drafting the
committed ThousandEyes snapshot's tag resource into a validating fluent
blueprint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…embedded kiota SDK The second pilot proves the whole kiota chain against reality. sdk generate derives an embedded SDK from the same pinned snapshot the blueprints came from (scoped to /tags, client ThousandEyesClient; kiota-lock.json committed), blueprints/thousandeyes-kiota rebinds the curated tag schema onto fluent chains -- Tags().ById(id).Get/Put/Delete, Tags().Post -- with method access, kiota's enum types behind parse companions, *uuid.UUID and *time.Time fields and both live TypeEscaped manglings, and bindings check verifies all 34 bindings against the real generated SDK. probe verify re-derives identical facts from the shared recordings/thousandeyes tree: switching dialect is a binding change, not an evidence change. The support shell (client with a static bearer AccessTokenProvider, errors keyed on ApiErrorable status codes with ErrEmptyResponse, convert with the Stringer/time/kiota-enum helpers, the acceptance harness, provider and main) is hand-written and owned by the pilot, as the boundary doctrine has it, and the module builds and tests green. Standing the pilot up surfaced and fixed three real emitter gaps: the state mapper hard-coded a pointer before the response type (wrong for interface-typed kiota responses -- the view now carries the full parameter type), the nested expand helper declared a local slice named 'models' that shadowed any models package alias (renamed elements, and elements are now built by ConstructorExpr under a setter SDK instead of calling methods on a nil interface), and the pre-declared shared diagnostics variable was unused when every conversion in a loop declares its own (the views now track shared-d use separately). A single nested attribute's wire conversion under kiotaFluent is refused by validation until it is rendered. The resty pilot regenerates byte-stable apart from the local rename, and both drift checks are green; CI gains a pinned-kiota SDK drift job (version and sha256 recorded in the workflow) and covers the second pilot in the regenerate and bindings jobs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
acceptance-kiota.yml runs the kiota pilot's generated acceptance tests against the live tenant, mirroring acceptance.yml rule for rule: hand- triggered or weekly-scheduled, never pull_request, gated on the same 'acceptance' environment whose THOUSANDEYES_* secrets it reuses -- one tenant, one credential, two dialects. A separate workflow rather than a matrix leg, deliberately: the resty run passing while this one fails is the single most useful signal it can produce, naming the broken dialect without reading logs. Both workflows share ONE concurrency group, because both create tfacc-prefixed objects in the same tenant and two live runs would fight over state that is not Terraform's; the kiota cron sits at 07:30 Monday behind resty's 06:00 only to keep the queue short. docs/checks.md records the pairing and the local reproduction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The generator could only bind the hand-written resty-style
go-sdk-thousandeyes. This PR makes the SDK itself derivable from the pinned OpenAPI snapshot with Microsoft Kiota, implements the long-reservedkiotaFluentdialect end to end, and adds the embed-vs-external choice for where a provider's SDK lives. A provider no longer requires an SDK to exist before onboarding an API.The new surface
tfpfgen sdk generate— a new noun group. Reads a pinned, checksum-verified snapshot and drives kiota as a gated PATH tool: the committedkiota-lock.jsonnames the version that produced the tree and the command refuses any other, naming both.-mode embed(default) generates under the provider's module — kiota's intended layout, module path derived from the enclosing go.mod;-mode externalemits a standalone tree with its own generated go.mod.-checkregenerates into scratch and byte-compares (normalising only the lock's output-relativedescriptionLocation). Output passes through the same gofumpt form the provider generator uses.The
kiotaFluentdialect, reserved → real, one phase per commit:Operation.chainrecords a fluent call as data (segments of method + args; the ID rides the typed indexer);ConvertCall.extraArgscarries enum parse companions;NestedAttributeObject.constructorExprbuilds setter-SDK elements;provider.sdkgainsmode/generator, andmodulePathis finally load-bearing (embed ⇒ equals the provider module; every service import must live under it). All additive — existing blueprints byte-identical.readExpr/writeStmt/expandStmt) behind every SDK field read/write;chainCallrenders fluent chains for all five block kinds and the acceptance helper; nil-result guards where kiota's(nil, nil)is meaningful (read ⇒ not-found, create ⇒ refuse to record, list ⇒ empty). Resty goldens proven byte-identical; the refusals lifted together only once the capability was whole.MethodChainwalks builder chains on real method sets;LookupAccessorPairverifies Get/Set pairs and surfaces mangling misses (GetErrorvsGetErrorEscaped) with the true spelling; write targets demand setters, read interfaces legitimately carry getters alone. Static facts are dialect-gated with a stated reason. A miniature kiota-shaped SDK in testdata exercises every path.blueprint draft -sdk-dialect kiotaFluent -sdk-models-package …derives chains from path templates, kiota's exact naming (plain capitalisation + keywordEscapedmangling), literal integer widths (*int32unlessformat: int64), and OpenAPI-driven enums wired through parse companions.source.sdkVersion; embedded kiota: the runtime module), failure naming the exactgo get.pilot/thousandeyes-kiota— the proof. Thetagresource end to end on an embedded, tags-scoped SDK:bindings checkverifies all 34 fluent bindings against the real generated code (including both liveTypeEscapedcases,*uuid.UUID,*time.Timeand four int-backed enums), the module builds and tests green,provider generate -checkis stable, andprobe verifyre-derives identical facts from the sharedrecordings/thousandeyes— switching dialect is a binding change, not an evidence change. Standing it up caught three real emitter gaps (pointer-to-interface state mappers, amodelslocal shadowing the models alias plus nil-interface element construction, and an unused shared diagnostics var), all fixed with the resty pilot regenerating stably.CI: a pinned-kiota SDK-drift job (version + recorded sha256 in the workflow env), and the second pilot joins the regenerate and bindings jobs. Docs updated throughout (cli.md drift-tested, architecture, glossary, blueprint.md, onboarding, README, CHANGELOG).
Verification
provider generate -checkgreen before and after).sdk generate -check,provider generate -check(both pilots) andprobe verify(shared recordings, kiota blueprint) all pass locally.Not yet: single-nested attribute wire conversion under
kiotaFluent(refused by validation with a message), a live acceptance run against the sandbox, and resource-by-resource expansion beyondtag— deliberately incremental per the plan.🤖 Generated with Claude Code