Skip to content

feat(cli): add opt-in respect-operation-id-word-boundaries OpenAPI setting - #17557

Open
fern-api[bot] wants to merge 8 commits into
mainfrom
devin/1787866024-operation-id-word-boundaries
Open

feat(cli): add opt-in respect-operation-id-word-boundaries OpenAPI setting#17557
fern-api[bot] wants to merge 8 commits into
mainfrom
devin/1787866024-operation-id-word-boundaries

Conversation

@fern-api

@fern-api fern-api Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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:

tag `sharing`         Sharing_ListFolderMembers          -> ["sharing", "listfoldermembers"]
                      tag prefix stripped, word boundaries lost -> /sharing/listfoldermembers

tag `files`           filesGetThumbnail                  -> ["files","get","thumbnail"]  (prefix stripped)
tag `files`           filesGetThumbnailV2                -> ["filesgetthumbnailv2"]      (prefix NOT stripped)

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.

api:
  specs:
    - openapi: openapi.yml
      settings:
        respect-operation-id-word-boundaries: true

With the setting on, tokenization uses lodash.words, which splits on separators, camelCase transitions and digits regardless of the shape of the input:

tag operationId default opt-in
sharing Sharing_ListFolderMembers listfoldermembers listFolderMembers
files filesGetThumbnailV2 filesGetThumbnailV2 getThumbnailV2
file_properties FileProperties_TemplatesGetForUser FileProperties_TemplatesGetForUser templatesGetForUser

Two 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.toApiSectionNode uses 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 off this.context.settings. The broader grouping algorithms are deliberately not unified: getEndpointLocation and computeGroupNameFromTagAndOperationId differ 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: toApiSectionNode falls 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:

SDK    client.sharing.listfoldermembers()  ->  client.sharing.listFolderMembers()
docs   /sharing/listfoldermembers          ->  /sharing/list-folder-members
  • That is a breaking SDK rename for an existing customer and belongs at a major bump; their docs URLs change at the same time, so they want redirects on their own site.
  • It is off by default and per-spec (settings: on an api spec in generators.yml), so no existing site or SDK changes unless it is explicitly enabled for that spec.
  • v-2 -> v2 is explicitly out of scope: that comes from lodash.kebabCase downstream in fern-platform (kebabCase("getThumbnailV2") === "get-thumbnail-v-2"), and words("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

  • New shared tokenizeOperationId(input, respectWordBoundaries?) in @fern-api/core-utils; the two private copies in getEndpointLocation.ts and AbstractOperationConverter.ts are deleted in favour of it, and the default branch is byte-for-byte the old behaviour.
  • AbstractOperationConverter reads this.context.settings.respectOperationIdWordBoundaries — no new plumbing, the settings object already reaches OpenAPIConverterContext3_1 via getOpenAPISettings.
  • getEndpointLocation(endpoint, options?) threads the new EndpointLocationOptions; buildServices / buildWebhooks / generateOverridesContent pass it from context.options.
  • Prefix stripping is skipped when it would leave a leading digit (files2GetThumbnail with tag files), which would otherwise produce an invalid identifier.
  • New respectOperationIdWordBoundaries option (defaults to false) plumbed through the OpenAPI settings schema, the serialized generators.yml schemas, convertGeneratorsConfiguration, getAPIDefinitionSettings, LegacyApiSpecAdapter, and the OSS / browser-compatible workspaces.
  • fern/apis/generators-yml/definition/generators.yml + regenerated generators-yml.schema.json / fern-yml.schema.json. Both regenerated files diff against main by nothing except the new setting.
  • CLI changelog entry (feat).
  • Updated README.md generator (if applicable)

Testing

  • Unit tests — core-utils/src/__tests__/tokenizeOperationId.test.ts pins both branches of the helper; openapi-to-ir/.../__test__/AbstractOperationConverter.test.ts pins 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.ts does 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 as parameter-content from feat(openapi): add respect-parameter-content setting to type parameters declared with content #17327 and per-spec-base-path from feat(cli): add respect-per-spec-base-path setting for per-spec x-fern-base-path #17495); the committed snapshots pin listFolderMembers, getThumbnailV2, templatesGetForUser, addV2, tokenFromOauth1.

  • Default gating, v2 side — re-ran the whole importer suite with --update across 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-definition against 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.

    user-endpoints/files/files-get-thumbnail-v-2                     -> user-endpoints/files/get-thumbnail-v-2
    user-endpoints/auth/auth-deprecated-token-from-oauth-1           -> user-endpoints/auth/token-from-oauth-1
    business-endpoints/team/team-team-members-add-v-2                -> business-endpoints/team/members-add-v-2
    user-endpoints/sharing/sharing-remove-file-member-2              -> user-endpoints/sharing/remove-file-member-2
    

    and the generated TS SDK renames the same endpoints (sharing.listfoldermembers() -> listFolderMembers(), files.filesGetThumbnailV2() -> getThumbnailV2()).

  • pnpm fern:build, pnpm check and pnpm format:check clean. Two url-reference snapshot tests fail locally only because that fixture $refs raw.githubusercontent.com, which is blocked in the sandbox; they pass in CI.


Devin Review

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Devin Review

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-09-02T04:06:59Z).

Fixture main PR Delta
docs 261.5s (n=5) 243.5s (35 versions) -18.0s (-6.9%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-09-02T04:06:59Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-09-02 18:15 UTC

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-09-02T04:06:59Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 97s (n=5) 118s (n=5) 66s -31s (-32.0%)
go-sdk square 149s (n=5) 311s (n=5) 147s -2s (-1.3%)
java-sdk square 244s (n=5) 315s (n=5) 228s -16s (-6.6%)
php-sdk square 79s (n=5) N/A 61s -18s (-22.8%)
python-sdk square 152s (n=5) 255s (n=5) 139s -13s (-8.6%)
ruby-sdk-v2 square 91s (n=5) 142s (n=5) 93s +2s (+2.2%)
rust-sdk square 225s (n=5) 228s (n=5) 161s -64s (-28.4%)
swift-sdk square 80s (n=5) 462s (n=5) 61s -19s (-23.8%)
ts-sdk square 178s (n=5) 187s (n=5) 181s +3s (+1.7%)

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 fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-09-02T04:06:59Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-09-02 18:16 UTC

@fern-api

fern-api Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the Devin Review + nitpickybot findings in adf81f4:

  • generateOverridesContent (writeOverridesForWorkspaces.ts) now receives respectOperationIdWordBoundaries from the per-spec settings (falling back to the workspace value), so generated x-fern-sdk-method-name overrides agree with the SDK method names. That was the only unthreaded getEndpointLocation caller; both tokenizeString callers were already threaded (the same-named function in openapi-to-ir's AbstractOperationConverter is a separate private implementation on the v3 path).
  • Stripping the tag prefix is now skipped when the remainder would start with a digit (files2GetThumbnail stays intact rather than becoming 2GetThumbnail), with a test.
  • Removed the redundant compact() around words().

Not changed: endpoint-name collisions (filesGetThumbnailV2 vs getThumbnailV2 under the same tag). FernDefinitionBuilder.addEndpoint overwrites service.endpoints[name] silently, and that's already the case on the legacy tokenizer path, so this isn't new in kind; adding detection means a cross-endpoint pre-pass in buildServices. Say the word if you'd rather have that in this PR than as a follow-up.

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.

0 participants