Skip to content

fix: /documentation NameError from sinatra-contrib 4.2.1 namespace bug#213

Merged
alexskr merged 7 commits into
developfrom
fix/documentation-route-namespace-issue-212
Apr 23, 2026
Merged

fix: /documentation NameError from sinatra-contrib 4.2.1 namespace bug#213
alexskr merged 7 commits into
developfrom
fix/documentation-route-namespace-issue-212

Conversation

@alexskr

@alexskr alexskr commented Apr 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Extracts home-controller helpers out of namespace "/" into a new helpers/home_helper.rb (Sinatra::Helpers::HomeHelper). Fixes the primary bug: /documentation and /metadata/:class raising NameError: undefined local variable or method 'metadata_all' under sinatra-contrib 4.2.1 + mustermann 3.0, because the namespace's before-filter compiles for "/" to a pattern that only matches the root path, so the module that carries the helpers is never mixed into the request instance for sub-paths. Same structural fix agroportal used.
  • Drops the dead cardinality column from HomeHelper#metadata_all. The producer (goo's cls.cardinality(attr)) was removed in Refactor: clean model settings module code agroportal/goo#52 and synced into ncbo/goo via Sync: align ncbo/goo with the AgroPortal codebase goo#176 (April 2024); the consumer views never read values[:cardinality] anyway. See issue comment for details.
  • Swaps pandoc-ruby for kramdown in the Gemfile. The :markdown filter in views/documentation/documentation.haml was resolving to Tilt's PandocTemplate, which shells out to a pandoc binary that isn't present in our ontoportal/testkit-base image (agroportal installs it explicitly in their Dockerfile; we'd rather avoid a system-binary dep for four prose blocks). kramdown is pure Ruby, already known to Tilt, no compile step, no cross-arch churn.
  • Adds a regression test test_documentation_route_renders to test/controllers/test_home_controller.rb so this doesn't silently regress again.

Fixes #212

Bonus: also restores long-broken /metadata/:class endpoints

The same namespace fix revives GET /metadata/:class for every valid LinkedData::Models::* class, e.g.

These URLs are listed in the long-standing #37 ("broken metadata resources", open since 2017). They've been returning 500s for years — under the prior Sinatra 1.4 stack the specific failure was different, but the user-visible symptom was the same, and the namespace-helper-resolution bug in Sinatra 4.2.1 kept them broken post-migration too. This PR closes out the class-name cases from that issue.

Note: issue #37 also lists property-name paths like /metadata/prefixIRI, /metadata/name, /metadata/omv*. Those aren't valid model class names and previously 500'd with a NameError; this PR makes them return a clean 404 with the message 'X' is not a documented media type. See /documentation or try /metadata/Ontology. (commit d2e5b64). Making /metadata/:name actually render documentation for those attribute URIs — which are legitimately emitted by the JSON-LD serializer in @context — is a separate design question tracked in ncbo/bioportal-project#388.

Related: #37, ncbo/bioportal-project#388

Commits

SHA Change
b85ee2b extract 10 helpers + CLASS_MAP into helpers/home_helper.rb; add regression test test_documentation_route_renders
6303343 drop dead cardinality column from HomeHelper#metadata_all (producer removed in goo 2024)
99d0df1 pandoc-rubykramdown in Gemfile/Gemfile.lock
5ca5f8d add regression tests for /metadata/:class (Metrics, Reply)
c37e214 resolve nested model classes in HomeHelper#metadata (sub-module walk for Notes::Reply etc.)
d2e5b64 return 404 from /metadata/:class for non-class names, worded with the BioPortal docs "media type" vocabulary and routed through the error helper
2b105ef broaden happy-path tests (Ontology, Metrics, Class); narrow 404 test to guaranteed-bogus names so future attribute-documentation work (ncbo/bioportal-project#388) doesn't read as a regression

Test plan

  • ruby -c on all changed Ruby files.
  • Structural repro (minimal Sinatra 4.2.1 app with the same namespace "/" + helpers Module shape) confirms the NameError is gone — GET /documentation returns 200 where it was 500.
  • Post-swap verification: Tilt[:md] resolves to Tilt::KramdownTemplate (not Tilt::PandocTemplate).
  • bundle exec ruby -Itest test/controllers/test_home_controller.rb --verbose against the 4store dev backend (via rake 'test:docker:up[fs]') — all 6 tests pass (32 assertions, 0 failures, 0 errors):
    • test_documentation_route_renders
    • test_metadata_route_renders_for_model_class (GET /metadata/Ontology, /metadata/Metrics, /metadata/Class)
    • test_metadata_route_resolves_submodule_class (GET /metadata/Reply → LinkedData::Models::Notes::Reply via the sub-module walk added in c37e214)
    • test_metadata_route_returns_404_for_bogus_names (guaranteed-nonexistent identifiers return 404 with the media type message)
    • test_home_index_returns_links_hash
    • test_home_index_handles_type_uri_failures
  • Manual smoke: bundle exec rackup and curl /, /documentation, /metadata/Class, /metadata/Metrics, /metadata/Slice — all 200.

Notes on deviations from agroportal

We adopted agroportal's structural fix (helper module extraction) verbatim but deviated on three secondary points:

Their choice Ours Why
Rename metadata_allget_metadata_all Keep metadata_all Cosmetic; would force a view edit for no benefit
Hardcoded routes_cls list in get_metadata_all Keep dynamic ObjectSpace discovery via routes_by_class Our GET / and /metadata/:class routes still rely on dynamic route→class mapping; agroportal removed those routes (MOD API catalog serving)
Install pandoc in Dockerfile Swap gem to kramdown Avoids a system-binary dep across every container (prod, CI, testkit base); pure-Ruby is a better match for the trivial use case

@alexskr
alexskr requested a review from mdorf April 22, 2026 22:46
@alexskr

alexskr commented Apr 23, 2026

Copy link
Copy Markdown
Member Author

Pre-fix review flagged that HomeHelper#metadata(cls) wouldn't resolve nested model classes (e.g. LinkedData::Models::Notes::Reply for /metadata/Reply), which would both break the #37 "bonus" claim and the test_metadata_route_resolves_submodule_class test.

Addressed in 65e4379: extracted a resolve_model_class(name) helper that first tries LinkedData::Models.const_get(name) and, on NameError, falls back to scanning LinkedData::Models.constants for a sub-module that defines the class — mirroring the same sub-module search routes_by_class already does.

Verified against the 4store backend:

TestHomeController#test_metadata_route_resolves_submodule_class   PASS (0.21s)   # GET /metadata/Reply
TestHomeController#test_metadata_route_renders_for_model_class    PASS (0.03s)   # GET /metadata/Metrics
TestHomeController#test_documentation_route_renders               PASS (0.07s)
TestHomeController#test_home_index_returns_links_hash             PASS (0.10s)
TestHomeController#test_home_index_handles_type_uri_failures      PASS (0.00s)
5 tests, 17 assertions, 0 failures, 0 errors

Two small stylistic choices worth noting:

  • Extracted the lookup into a named helper rather than inlining in metadata, so the sub-module search logic has one home (future callers or routes_by_class itself could consolidate onto it).
  • Iteration is "first wins" rather than routes_by_class's implicit "last wins". No practical difference — no duplicate class names across LinkedData::Models::* sub-modules in the current codebase — but happy to flip it to match routes_by_class exactly if that's preferred.

alexskr added 7 commits April 22, 2026 21:55
Under sinatra-contrib 4.2.1 + mustermann 3.0, the before-filter that
Sinatra::Namespace uses to mix its namespace module into each request
instance (`before { extend(@namespace = namespace) }`) compiles for
`namespace "/"` to a pattern that matches only `/` and not sub-paths
like `/documentation` or `/metadata/:class`. Methods defined with bare
`def` inside the namespace block were therefore unreachable from those
routes, producing:

  NameError: undefined local variable or method `metadata_all'
             for #<Sinatra::Application:...>

Move the home-controller helpers (metadata_all, metadata,
resource_collection_link, sample_objects, hypermedia_links,
routes_by_class, route_to_class_map, routes_list, parse_route,
safe_type_uri, and CLASS_MAP) out of `namespace "/"` into a proper
`Sinatra::Helpers::HomeHelper` module registered via the top-level
`helpers` DSL — the same shape every other helper in this repo uses.
Helpers now attach to Sinatra::Application at class-load time and no
longer depend on the namespace before-filter firing.

Add a regression test that exercises GET /documentation.

Fixes #212
The producer (`cls.cardinality(attribute)`) was removed from goo in the
AgroPortal fork in agroportal/goo#52 (commit 2206084, merged
2024-01-31) and subsequently synced into ncbo/goo via ncbo/goo#176 on
2024-04-03. The consumer — the `views/documentation/*.haml` tables —
has no reference to `values[:cardinality]` either. The key in the
attributes_info hash was being populated only to be thrown away.

Delete the key from both branches of the conditional. No view change
needed; the earlier `rescue nil` guard added in 1fe44f8 becomes
unnecessary and is removed with it.
`views/documentation/documentation.haml` contains four `:markdown`
filter blocks that HAML delegates to Tilt. In the current bundle the
only markdown-capable gem was `pandoc-ruby`, so Tilt selected its
PandocTemplate, which shells out to an external `pandoc` binary. Our
Docker image inherits from `ontoportal/testkit-base` and does not
install that binary, so `/documentation` failed with
`RuntimeError: sh: pandoc: command not found` in any environment
without pandoc on the PATH.

Replace `pandoc-ruby` with `kramdown`, a pure-Ruby renderer already
known to Tilt (`tilt/kramdown.rb`). This removes the system-binary
dependency entirely rather than matching agroportal's approach of
installing pandoc in their Dockerfile. Performance is indistinguishable
for four short blocks on a rarely-hit developer page; pure-Ruby avoids
cross-architecture compile/install churn across containers.

Verified post-install: `Tilt[:md] → Tilt::KramdownTemplate`.
The home-helper extraction also restores `GET /metadata/:class`, which
had been broken for years (#37) and was broken again
for a different reason under sinatra-contrib 4.2.1
(#212). Lock the fix in with two tests:

- /metadata/Metrics — exercises the common case (singularize +
  top-level LinkedData::Models::Metric lookup).
- /metadata/Reply — exercises HomeHelper#routes_by_class's sub-module
  search branch (LinkedData::Models::Notes::Reply).
`metadata(cls)` did a bare `LinkedData::Models.const_get(name)`, so
requests like `GET /metadata/Reply` blew up with
`NameError: uninitialized constant LinkedData::Models::Reply` —
`Reply` lives under `LinkedData::Models::Notes`, not at the top level.
`routes_by_class` already has sub-module search for the same reason;
`metadata` was missing the mirror.

Extract a small `resolve_model_class(name)` helper that tries the
top-level lookup first and falls back to scanning
`LinkedData::Models.constants` for the name, matching the behavior of
`routes_by_class`. This restores `GET /metadata/Reply` and any other
nested model class (e.g. `Users::Role`, `Users::NotificationType`).

test_metadata_route_resolves_submodule_class now passes.
The previous "No metadata for \`X\` — not a LinkedData model class"
leaked the Ruby gem name and used "model class", terms API users don't
encounter elsewhere. The BioPortal /documentation page uses "media
type" for exactly these /metadata/:X URIs (they surface as JSON-LD
@type / @context identifiers in responses, documented under "Media
Types and Hypermedia Links"), so match that vocabulary and point the
user at a concrete working example.

Also route the response through the repo's `error` helper so the 404
comes back as the standard `{ errors: [...], status: 404 }` shape
through LinkedData's content-negotiating serializer, matching every
other error response in the codebase (no more one-off `halt 404,
"string"`).

Strengthen the regression test to assert the new phrasing appears in
the body, not just the status code.
- Extend `test_metadata_route_renders_for_model_class` to loop over
  `Ontology`, `Metrics`, `Class` — covers plain top-level lookup
  (`Ontology`), singularization (`Metrics` → `Metric`), and a second
  plain case with a distinctive name (`Class`). All three hit the same
  `resolve_model_class` top-level branch, but `Ontology` was the one
  case we previously had no explicit coverage for.

- Rename `test_metadata_route_returns_404_for_non_class_names` →
  `test_metadata_route_returns_404_for_bogus_names` and trim its list
  to `DefinitelyNotAModel`, `nonexistent_thing`, `xyzqux123` —
  guaranteed-nonexistent identifiers that will stay 404 regardless of
  future features.

  The previous list (`created`, `body`, `prefixIRI`, `name`,
  `omvacronym`) was misleading: those *are* attribute URIs the API
  emits in JSON-LD @context, currently 404'd as a stop-gap but tracked
  in ncbo/bioportal-project#388 for actual attribute-documentation
  rendering. Leaving them asserted as 404 would turn into a false
  regression signal when #388 lands.
@alexskr
alexskr force-pushed the fix/documentation-route-namespace-issue-212 branch from 619c1f1 to 2b105ef Compare April 23, 2026 05:13
@alexskr
alexskr merged commit ce24c18 into develop Apr 23, 2026
10 checks passed
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