Skip to content

Lazy-load models and API clients with autoload - #380

Open
ngan wants to merge 1 commit into
XeroAPI:masterfrom
ngan:np-autoload
Open

Lazy-load models and API clients with autoload#380
ngan wants to merge 1 commit into
XeroAPI:masterfrom
ngan:np-autoload

Conversation

@ngan

@ngan ngan commented Aug 5, 2026

Copy link
Copy Markdown

Problem

require 'xero-ruby' loads all 543 models, 10 API clients, and — via api_client.rb — faraday and json-jwt. That's ~180k LOC parsed to call one endpoint, regardless of which API set you use. Adding the gem to a Gemfile without require: false costs ~58 MB of RSS.

Change

Register everything in lib/xero-ruby.rb with autoload: models, API clients, and the shared classes. There's no load-time coupling to work around — ApiClient#deserialize and each model's _deserialize resolve types through const_get, which triggers autoload — so this is invisible to callers.

Savings

Ruby 3.4.10, RSS delta from a bare interpreter, best of 3:

before after
require 'xero-ruby' 57.9 MB / 1.29 s / 1249 files 1.0 MB / ~0.00 s / 160 files
+ Accounting::Invoice 57.9 MB 1.7 MB
+ ApiClient 57.9 MB 23.0 MB (faraday + json-jwt)
+ ApiClient & Accounting API 57.9 MB 31.8 MB

Consumers who genuinely use every model end up where they started — no regression at the ceiling.

Two latent bugs fixed

Eager loading was masking these. Both surface once the shared classes can load independently:

  • configuration.rb calls Logger.new without require 'logger'Configuration.default raised NameError: uninitialized constant XeroRuby::Configuration::Logger
  • where.rb branches on Date/DateTime without require 'date'Where#to_param raised on any Array or Range value

Both worked only because api_client.rb was loaded first and happened to require them.

Notes

  • configure moves to a Singleton module extended at the bottom of the entry point.
  • Autoload paths are absolute (__dir__), so Ruby resolves them directly rather than searching $LOAD_PATH. No double-load against the gemspec's relative require "xero-ruby/version", verified in both orders.
  • The autoload list mirrors the existing require list rather than globbing models/, so the public constant surface is unchanged — 559 constants, diffed before and after.
  • Constant names are read from each source file, not camelized from filenames — cis_settings.rb defines CISSettings, ni_category.rb defines NICategory, api_exception.rb defines APIException. Camelizing breaks 10 files.
  • Heads-up, unrelated to this change: globbing models/ would pick up 10 Finance::* models whose requires were dropped in 13.0.0 (8c3bd9f) but whose files were never deleted. They have no return_type references and raise NameError on master today. This PR preserves that.

Verification

3197 examples, 0 failures. bundle exec rubocop clean (1030 files). find . -name "*.rb" | xargs -n 1 ruby -c passes.

On generated code

Per CONTRIBUTING, lib/xero-ruby.rb comes from your mustache templates, so the durable fix is a template change rather than this diff — the next release would regenerate the file and silently revert to eager loading (nothing breaks, it just gets slow and fat again). Happy to port this to the template instead, or open an issue to discuss first if that's preferred; this PR is mainly here to show the measurements and that the approach is sound.

Requiring the gem loaded all 543 models and 10 API clients for every Xero
API, whether or not the consumer touched them, plus faraday and json-jwt via
api_client.rb. That is ~180k LOC and ~58 MB to call one endpoint, and it is
why adding this gem to a Gemfile without `require: false` is expensive.

Register everything with `autoload` instead -- models, API clients, and the
shared classes. Nothing is referenced at load time: ApiClient#deserialize and
each model's `_deserialize` resolve types through `const_get`, which triggers
autoload transparently, so this is invisible to callers.

Measured on Ruby 3.4.10:

                              before      after
  require 'xero-ruby'        57.9 MB     1.0 MB    (1249 -> 160 files)
                                1.29s     0.00s
  + Accounting::Invoice                  1.7 MB
  + ApiClient                           23.0 MB    (faraday + json-jwt)
  + ApiClient & Accounting              31.8 MB

Making the shared classes lazy exposed two latent bugs that eager loading had
been masking, both fixed here: configuration.rb calls Logger.new without
requiring 'logger', and where.rb branches on Date and DateTime without
requiring 'date'. Both previously worked only because api_client.rb was loaded
first and happened to require them.

`configure` moves to a Singleton module that is extended at the bottom of the
entry point, following the pattern in Gusto/fixture_kit.

Autoload paths are absolute, so Ruby resolves them directly instead of
searching $LOAD_PATH. The list mirrors the previous require list rather than
globbing the model directory, keeping the public constant surface unchanged at
559 constants. Note that globbing would also pick up 10 Finance models whose
requires were dropped by the 13.0.0 release but whose files were never
deleted; they have no return_type references and raise NameError today, so
that is preserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCuhKDE1ZdU7s6seTURJx3
@ngan ngan mentioned this pull request Aug 5, 2026
@SerKnight

Copy link
Copy Markdown
Contributor

Nice one

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.

2 participants