Skip to content

feat!: rebuild the client on API specification 2.26.1 - #30

Merged
gierschv merged 1 commit into
masterfrom
fix/faraday-dependencies
Sep 11, 2026
Merged

feat!: rebuild the client on API specification 2.26.1#30
gierschv merged 1 commit into
masterfrom
fix/faraday-dependencies

Conversation

@gierschv

@gierschv gierschv commented Sep 11, 2026

Copy link
Copy Markdown
Member

The Ruby equivalent of the Python 2.0.0 bootstrap. Publishes as 1.0.0 (RubyGems is at 0.3.5).

v2.26.1 is released. Regenerating with SPEC_REF=v2.26.1 BUMP=none ./tools/generate.sh produces this tree byte for byte.

Four defects, none of which any check in this repository would have caught

Every one was found by installing the gem and making real requests.

gem install pulled the wrong HTTP stack The gemspec declared typhoeus; tools/openapi-config.json selects the faraday library. It installed a dependency the code never loads, and skipped the ones it does. Now declares faraday, faraday-multipart, marcel.
The gem could not be loaded at all The 200 response of createLtiConfiguration was an inline allOf over LtiConfiguration, a oneOf. This generator emits a oneOf as a module and an allOf as a subclass, so the output read class CreateLtiConfiguration200Response < LtiConfiguration and Ruby refuses to inherit a module. Fixed in the specification (BE-1213), not patched here.
The typed errors were dead code errors.rb defined the hierarchy; api_client.rb went on raising the generated ApiError. Nothing could ever rescue FlatNotFoundError.
The whole ergonomic layer was unreachable None of errors.rb, retry.rb, pagination.rb, oauth.rb was required from lib/flat_api.rb. require 'flat_api' gave you the generated client and nothing built on it. 95_requires.py wires them, errors first, because api_client.rb raises from it.

That last one compounds the third: had the raise site been rewired without the requires, it would have thrown NameError at the exact moment it tried to report an API error.

Also

  • required_ruby_version 3.0 → 3.3, matching the runtimes upstream still supports and .sdkgen.yaml.
  • The gemspec ships LICENSE, README.md, CHANGELOG.md, and drops a test_files list pointing at a directory that does not exist.
  • Removes .gitlab-ci.yml: a leftover of an old generation. This repository has no GitLab mirror, the file pins Ruby 3.0 and 3.1 which the gemspec now refuses, and it runs rspec against a spec/ directory that is not there.
  • tools/smoke.rb drives the shared scenarios against the real API.
  • Stops tracking .DS_Store.
  • Removes a test harness that was never wired up: an rspec development dependency and a .rspec requiring a spec_helper that does not exist, a Rakefile task over a spec/ directory that does not exist, pry-byebug, and a rubocop pinned to 0.66.0 from 2019 whose config targets Ruby 2.4 and excludes Rails paths. Nothing ran any of it, and rubocop at that pin cannot parse the Ruby this gem now requires. It was also the source of the Socket Security alert on this PR (byebug, via pry-byebug), which now passes.

Second round: four more of the same, from the Codex review

Each was written, documented in the README, and never reached by a request. A build cannot tell a module that runs from one that does not, which is the whole problem.

FlatApi::FlatClient did not exist The first code sample in the README and in QUICKSTART raised NameError, as did the pagination example. 30_client.py writes it: one ApiClient, each generated API by short name (client.scores, client.omr), and paginate without the caller seeing a cursor. It builds its own Configuration rather than mutating Configuration.default, so two clients with different tokens do not overwrite each other
Pagination returned page 1 forever It sent the cursor as :next; the generator renames the option to :_next (next is a Ruby keyword) and maps it back to the query parameter itself. Every iteration refetched page one, the loop guard saw a repeated cursor, and the traversal stopped looking successful
RetryPolicy was never called call_api is now a wrapper around the renamed call_api_once, and Configuration#retry_policy tunes or disables it. One request path wrapped, not 127 generated methods
TokenManager#access_token ignored expiry It returned the token expired or not, making Tokens#expired? dead code. It now refreshes, re-checking inside the lock so two threads spend one round trip rather than two, and a provider that rotates refresh tokens does not have the second call invalidate what the first stored

The suite that missed all four, changed

  • It goes through FlatClient, so the path the docs point at is the path that is proven.
  • The pagination check creates three collections and traverses with limit: 1, so it cannot pass without following the cursor. The old one asked for ten items on an account holding fewer, never requested a second page, and proved nothing.
  • Two checks need no network and cover the retry wiring and the token refresh.
  • smoke/scenarios.yaml listed two scenarios no runner executed. It now describes what actually runs.

Also caught here

The retry patch was not idempotent: after patching, call_api is the wrapper, so testing for the entry point re-wrapped it every run, and twice through it produced a call_api_once that called itself. check_idempotency.sh found it. It now keys off an explicit marker.

Verification

check result
ruby:3.3 / ruby:3.4, frozen bundle install, require 'flat_api' pass, resolves FlatApi::FlatNotFoundError
check_determinism.sh pass
check_idempotency.sh pass
sdk-lint-spec pass, 161 schemas
sdk-check / sdk-check-docs / sdk-enforce-zones / sdk-validate-manifest pass (123 operations, docs 122/122)
FlatClient from the installed gem on 3.3 and 3.4 the README's first example runs verbatim
smoke against production not re-run for this round (see below)

⚠️ The production smoke passed on the first round, but has not been run against the current tree. Its token is a masked CI variable I cannot read back. The new pagination, retry and OAuth checks have not executed against production. To run it:

cd ../api-client-gen
FLAT_TEST_TOKEN=<token> ./smoke/run.sh --language ruby --sdk-path ../api-client-ruby

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-11T08:04:26.802848Z a13c24f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@socket-security

socket-security Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedrake@​13.0.6 ⏵ 13.4.290 +1100100100100
Updatedflat_api@​0.3.5 ⏵ 1.0.0N/AN/AN/AN/AN/A

View full report

@socket-security

socket-security Bot commented Sep 11, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@gierschv
gierschv force-pushed the fix/faraday-dependencies branch from a13c24f to a6437f5 Compare September 11, 2026 08:02

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a13c24f379

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md

```shell
gem install ./flat_api-0.3.0.gem
client = FlatApi::FlatClient.new(access_token: 'YOUR_TOKEN')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Export the client used by the quickstart

Following this newly published setup raises NameError: a repo-wide search finds no FlatApi::FlatClient definition, while the package root exports only the generated *Api classes. Consequently, every user following either the README or QUICKSTART fails before making a request; implement and export this wrapper or document the actual Configuration/ApiClient setup.

Useful? React with 👍 / 👎.

page_params = cursor ? params.merge(next: cursor) : params
data, _status, headers = fetch_page.call(page_params)
Array(data).each { |item| yielder << item }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pass cursors through the generated _next option

For any response containing a next-page link, this passes the cursor as :next, but the generated paginated methods only copy opts[:_next] into the next query parameter (for example, CollectionApi#list_collections_with_http_info). The second iteration therefore repeats the first request, yields duplicate items, and then stops when seen encounters the same cursor instead of traversing subsequent pages.

Useful? React with 👍 / 👎.

fail ApiError.new(code: 0,
message: response.return_message)
else
fail FlatApi.error_from_response(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Invoke the retry policy before raising transient failures

When an idempotent request receives a 5xx response or the Flat-specific rate-limit 403, this path immediately raises the typed error. RetryPolicy is never referenced from ApiClient or elsewhere, so its attempt count, backoff, and reset handling are dead code and the automatic retries advertised in the README and changelog never occur.

Useful? React with 👍 / 👎.

Comment thread lib/flat_api/oauth.rb
Comment on lines +93 to +95
def access_token
@tokens.access_token
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refresh tokens when access_token observes expiry

When expires_at has passed, access_token still returns the expired token without consulting Tokens#expired? or invoking refresh; neither expiry check is used anywhere else in the repository. Even if a caller installs this method as Configuration#access_token_getter, requests continue using the stale bearer token and receive 401 responses rather than getting the promised transparent refresh.

Useful? React with 👍 / 👎.


# reconstruct content
content = stream.join
content = content.unpack('m').join if response.headers['Content-Transfer-Encoding'] == 'binary'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve response bodies marked as binary

When a file endpoint responds with Content-Transfer-Encoding: binary, the payload already consists of raw octets, but unpack('m') treats it as base64 and decodes it. Such score exports, OMR files, or class exports are therefore returned corrupted; base64 decoding should only occur for a base64 transfer encoding.

Useful? React with 👍 / 👎.

@gierschv
gierschv force-pushed the fix/faraday-dependencies branch from a6437f5 to cdeea25 Compare September 11, 2026 08:33
The 0.3.x line was generated in 2024 and has drifted well past usefulness: it
misses every operation the API has shipped since, including OMR, and it offers
nothing above the raw generated surface. This replaces it with a generated
client that is reproducible from this repository alone, plus the ergonomic
layer the README has always claimed.

Regenerated with openapi-generator 7.24.0 (ruby, faraday) against API
specification 2.26.1: 123 operations, 196 models, 23 scopes.

Four defects that no check in this repository would have caught, all found by
installing the gem and making real requests:

  * The gemspec declared typhoeus while tools/openapi-config.json selects the
    faraday library. `gem install flat_api` pulled a dependency the code never
    loads, and did not pull the ones it does. Now declares faraday,
    faraday-multipart and marcel.

  * The gem could not be loaded at all. The 200 response of
    createLtiConfiguration was an inline allOf over LtiConfiguration, which is
    a oneOf. This generator emits a oneOf as a module and an allOf as a
    subclass, so the output read `class CreateLtiConfiguration200Response <
    LtiConfiguration`, and a Ruby class cannot inherit a module. Fixed in the
    specification (BE-1213), not patched here.

  * The typed errors were dead code. errors.rb defined the hierarchy, and
    api_client.rb went on raising the generated ApiError, so nothing could ever
    rescue FlatNotFoundError. The raise site is rewired now.

  * None of errors.rb, retry.rb, pagination.rb or oauth.rb was required from
    lib/flat_api.rb, so `require 'flat_api'` gave you the generated client and
    nothing built on top of it. The retry policy, the pagination helper and the
    OAuth refresh were all unreachable. 95_requires.py wires them, errors
    first, because api_client.rb raises from it.

Also:

  * required_ruby_version moves from 3.0 to 3.3, matching the runtimes upstream
    still supports and the matrix in .sdkgen.yaml.
  * The gemspec ships LICENSE, README.md and CHANGELOG.md, and drops a
    test_files list pointing at a directory that does not exist.
  * Removes .gitlab-ci.yml. It is a leftover of an old generation: this
    repository has no GitLab mirror, the file pins Ruby 3.0 and 3.1, which the
    gemspec now refuses, and it runs rspec against a spec directory that is not
    there.
  * tools/smoke.rb drives the shared scenarios against the real API: score
    lifecycle, a paginated traversal and both typed errors, cleaning up what it
    creates. Nothing metered.
  * Stops tracking .DS_Store.
  * Removes the test harness that was never wired up: an rspec development
    dependency and a .rspec requiring a spec_helper that does not exist, a
    Rakefile task over a spec directory that does not exist, pry-byebug, and a
    rubocop pinned to 0.66.0 from 2019 with a config targeting Ruby 2.4 and
    excluding Rails paths. Nothing ran any of it, and rubocop at that pin
    cannot parse the Ruby this gem now requires. CI builds the gem and loads
    every file, which is the same shape as the Python client's.

A second round, after a review pointed at four more of exactly the same kind.
Each was written, documented in the README and never reached by a request:

  * FlatApi::FlatClient did not exist. The first code sample in the README and
    in QUICKSTART raised NameError, and so did the pagination example below it.
    30_client.py writes it: it holds one ApiClient, exposes each generated API
    by a short name, and paginates without the caller seeing a cursor. It builds
    its own Configuration rather than mutating Configuration.default, so two
    clients with different tokens do not overwrite each other.

  * Pagination sent the cursor as :next. The generator renames the parameter to
    :_next, because next is a Ruby keyword, and maps it back to the next query
    parameter itself. Every iteration therefore refetched page one; the loop
    guard saw a cursor it had already used and stopped. Traversals returned the
    first page and looked like they had reached the end.

  * RetryPolicy was never called. call_api is now a wrapper around the renamed
    call_api_once, and Configuration carries a retry_policy to tune or disable
    it. Wrapping the one request path rather than the 127 generated methods.

  * TokenManager#access_token returned the token whether or not it had expired,
    which made Tokens#expired? dead code. It now refreshes, re-checking inside
    the lock so that two threads arriving together spend one round trip rather
    than two, and so a provider that rotates refresh tokens does not have the
    second call invalidate what the first stored.

The smoke suite passed through all four, so it changed too:

  * It goes through FlatClient, the entry point the documentation points at.
  * The pagination check creates three collections and traverses with limit=1,
    so it cannot pass without following the cursor. The previous version asked
    for ten items on an account holding fewer, never requested a second page,
    and proved nothing.
  * Two checks that need no network cover the retry wiring and the token
    refresh, since a build cannot tell a module that runs from one that does
    not.
  * scenarios.yaml listed two scenarios no runner executed. It now describes
    what actually runs.
@gierschv
gierschv force-pushed the fix/faraday-dependencies branch from cdeea25 to 45d9629 Compare September 11, 2026 09:58
@gierschv
gierschv merged commit 7ae84e9 into master Sep 11, 2026
5 checks passed
@gierschv
gierschv deleted the fix/faraday-dependencies branch September 11, 2026 12:32
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