feat(cli): add opt-in respect-operation-id-word-boundaries OpenAPI setting - #17557
feat(cli): add opt-in respect-operation-id-word-boundaries OpenAPI setting#17557fern-api[bot] wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
AI Review Summary
Adds an opt-in respect-operation-id-word-boundaries setting that routes operationId tokenization through lodash.words. Plumbing looks complete and consistent (schemas, serialization, workspaces, options defaults), and the default path is untouched. Two things worth checking: whether every getEndpointLocation/tokenizeString call site is threaded with the option, and the new possibility of a leading-digit or colliding endpointId once digits become their own tokens.
- 🟡 2 warning(s)
To request another review, comment /ai-review on this pull request.
There was a problem hiding this comment.
Devin Review found 1 potential issue.
⚠️ 1 issue in files not directly in the diff
⚠️ Override generation ignores the new word-boundary setting
generateOverridesContent calls getEndpointLocation without passing respectOperationIdWordBoundaries, though the workspace carries it. With the setting enabled, generated x-fern-sdk-method-name overrides use the old names and pin values that disagree with the actual SDK method names.
Docs Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on
Docs generation runs |
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
…uard leading-digit ids
|
Addressed the Devin Review + nitpickybot findings in adf81f4:
Not changed: endpoint-name collisions ( |
Description
Linear ticket: Refs
The operationId tokenizer only splits on camelCase boundaries when the whole string is purely alphabetic camel/Pascal (
/^[a-z]+(?:[A-Z][a-z]+)*$/). Any underscore or digit routes it to the separator branch, which splits only on non-alphanumerics and lowercases each chunk. Two consequences, both reported by a customer with a 276-endpoint spec:so an endpoint's docs slug and SDK method name depend on whether its operationId happens to contain a digit or a separator. Fixing the tokenizer unconditionally would rename endpoints (and therefore SDK methods and docs URLs) for every existing customer, so this adds it as an opt-in OpenAPI spec setting, off by default.
With the setting on, tokenization uses
lodash.words, which splits on separators, camelCase transitions and digits regardless of the shape of the input:sharingSharing_ListFolderMemberslistfoldermemberslistFolderMembersfilesfilesGetThumbnailV2filesGetThumbnailV2getThumbnailV2file_propertiesFileProperties_TemplatesGetForUserFileProperties_TemplatesGetForUsertemplatesGetForUserTwo tokenizers: SDK path and docs path
That tokenizer was duplicated, once in the v2 importer (
getEndpointLocation.ts, which SDK generation goes through) and once privately inside the v3 OpenAPI converter (AbstractOperationConverter.computeGroupNameFromTagAndOperationId).DocsDefinitionResolver.toApiSectionNodeuses the v3 parser by default (openapiParserV3 == null || openapiParserV3), so docs slugs never touch the v2 code. An earlier revision of this PR only changed the v2 copy, which renamed SDK methods and left docs URLs — the actual reported symptom — untouched.This revision moves the tokenizer into
@fern-api/core-utils(tokenizeOperationId(input, respectWordBoundaries)), which both packages already depend on, and has the v3 converter read the same per-spec setting offthis.context.settings. The broader grouping algorithms are deliberately not unified:getEndpointLocationandcomputeGroupNameFromTagAndOperationIddiffer on file naming,x-fern-sdk-*handling and namespace rules, so merging them is a separate change with real risk to flag-off output and is not needed here. Only the word splitter is shared.Fallback caveat for reviewers:
toApiSectionNodefalls back to the v2 workspace when v3 produces no IR (if (ir == null)), so in principle the same spec can be grouped by either implementation — both now honour the setting. The two paths are not interchangeable in practice though: the customer's spec fails on the v2 parser (Expected one of ConflictErrorBodyErrorZero, ConflictErrorBodyErrorOne, ConflictErrorBodyErrorTwo), so for them a v3 failure is a broken publish rather than differently-grouped slugs. That v2 parser bug is pre-existing and out of scope here.Reviewer note: opting in is a breaking rename
The value this changes is the endpointId (v2) / group + method name (v3), which is what both the docs slug and the SDK method name are derived from. So for a customer who turns the setting on:
settings:on an api spec ingenerators.yml), so no existing site or SDK changes unless it is explicitly enabled for that spec.v-2->v2is explicitly out of scope: that comes fromlodash.kebabCasedownstream in fern-platform (kebabCase("getThumbnailV2") === "get-thumbnail-v-2"), andwords("V2")splits the digit too. Opting in gives/files/get-thumbnail-v-2, not/files/get-thumbnail-v2; a digit-aware kebab is a separate change that is not being made here.Changes Made
tokenizeOperationId(input, respectWordBoundaries?)in@fern-api/core-utils; the two private copies ingetEndpointLocation.tsandAbstractOperationConverter.tsare deleted in favour of it, and the default branch is byte-for-byte the old behaviour.AbstractOperationConverterreadsthis.context.settings.respectOperationIdWordBoundaries— no new plumbing, the settings object already reachesOpenAPIConverterContext3_1viagetOpenAPISettings.getEndpointLocation(endpoint, options?)threads the newEndpointLocationOptions;buildServices/buildWebhooks/generateOverridesContentpass it fromcontext.options.files2GetThumbnailwith tagfiles), which would otherwise produce an invalid identifier.respectOperationIdWordBoundariesoption (defaults tofalse) plumbed through the OpenAPI settings schema, the serializedgenerators.ymlschemas,convertGeneratorsConfiguration,getAPIDefinitionSettings,LegacyApiSpecAdapter, and the OSS / browser-compatible workspaces.fern/apis/generators-yml/definition/generators.yml+ regeneratedgenerators-yml.schema.json/fern-yml.schema.json. Both regenerated files diff againstmainby nothing except the new setting.feat).Testing
Unit tests —
core-utils/src/__tests__/tokenizeOperationId.test.tspins both branches of the helper;openapi-to-ir/.../__test__/AbstractOperationConverter.test.tspins the v3 group+method for the underscore, digit, multi-token-tag and non-matching-prefix shapes with the flag off and on;openapi-ir-to-fern/src/utils/__test__/getEndpointLocation.test.tsdoes the same for the v2 path plus the leading-digit guard.Importer fixture —
openapi-ir-to-fern-tests/src/__test__/fixtures/operation-id-word-boundaries/opts the setting on (same shape asparameter-contentfrom feat(openapi): add respect-parameter-content setting to type parameters declared with content #17327 andper-spec-base-pathfrom feat(cli): add respect-per-spec-base-path setting for per-spec x-fern-base-path #17495); the committed snapshots pinlistFolderMembers,getThumbnailV2,templatesGetForUser,addV2,tokenFromOauth1.Default gating, v2 side — re-ran the whole importer suite with
--updateacross every fixture: the only snapshot that changes is the new opted-in one, every non-opted-in fixture is byte-identical.Default gating, docs/v3 side — built the prod CLI and ran
write-docs-definitionagainst the reporting customer's real config with this revision and with the v3 converter reverted to its pre-change copy, flag off in both: the two docs definitions are byte-identical (350 unique slugs, incl. all 25 pre-existing stuttering slugs). Independently, that flag-off output matches released 5.109.1's published sitemap (333 URLs, zero differences).Docs slugs actually change with the flag on — same config, same CLI, only the setting added: 25 slugs change, e.g.
and the generated TS SDK renames the same endpoints (
sharing.listfoldermembers()->listFolderMembers(),files.filesGetThumbnailV2()->getThumbnailV2()).pnpm fern:build,pnpm checkandpnpm format:checkclean. Twourl-referencesnapshot tests fail locally only because that fixture$refsraw.githubusercontent.com, which is blocked in the sandbox; they pass in CI.