Skip to content

Pairs under an if / unless in a jbuilder template are emitted instead of dropped - #361

Merged
eddygarcas merged 1 commit into
rubys:mainfrom
eddygarcas:jbuilder-conditional-pairs
Oct 3, 2026
Merged

eddygarcas merged 1 commit into
rubys:mainfrom
eddygarcas:jbuilder-conditional-pairs

Conversation

@eddygarcas

@eddygarcas eddygarcas commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Probed with roundhouse 2026.9.18 (7fad14c0), Linux x86-64, CRuby 4.0.5.

In a jbuilder object template, a statement under a condition is dropped with no diagnostic: a block if … else … end, an unless, and the if / unless modifiers. An optional field (json.label x if …) or a branch between two values is the usual way a template writes either.

# app/views/widgets/branch.json.jbuilder
json.id @widget.id
if @widget.size
  json.big true
else
  json.big false
end

# app/views/widgets/modifier.json.jbuilder
json.id @widget.id
json.label @widget.name if @widget.size
json.note @widget.name unless @widget.size.nil?

# app/views/widgets/first_pair.json.jbuilder
if @widget.size
  json.big true
end
json.id @widget.id

Widget 1 has name: "b", size: 5, widget 2 name: "a", size: nil:

Rails 8.1.4 + jbuilder 2.15.1 (widget 1, widget 2) main
branch {"id":1,"big":true}, {"id":2,"big":false} {"id":1}, {"id":2}
modifier {"id":1,"label":"b","note":"b"}, {"id":2} {"id":1}, {"id":2}
first_pair {"big":true,"id":1}, {"id":2} {"id":1}, {"id":2}

classify (src/lower/jbuilder_to_library/mod.rs) only looks at json.* sends, so the If (ingest gives unless the same node with the branches swapped) is Unknown and becomes io << "".

Fix

A new statement kind, Cond, for an If statement of an object template. It emits the same if, each branch's pairs appended inside it:

io << "\"id\":"
io << JsonBuilder.encode_value(widget.id)
if widget.size
  io << ","
  io << "\"big\":"
  io << JsonBuilder.encode_value(true)
else
  io << ","
  io << "\"big\":"
  io << JsonBuilder.encode_value(false)
end

The commas needed one change. emit_object counted pairs statically (emitted > 0); after a conditional with a pair in one branch only, the next pair can't know at lowering time whether it is the first. The pair loop moves into emit_pairs, which threads a three-state Sep (First, After, Unknown) through the statements and both branches: a branch starts from the state before the if, and the state after it is the branches' common state, or Unknown when they differ. Only in the Unknown state is the comma decided at run time, by asking the accumulator (first_pair above):

if widget.size
  io << "\"big\":"
  io << JsonBuilder.encode_value(true)
end
io << "," if !(io.end_with?("{"))
io << "\"id\":"

That is exact because an object's pairs are the only thing appended after its {, and no complete JSON value ends in {. Templates without a conditional emit exactly what they did before (the comma state is First or After throughout). An unless (an If with an empty then-branch after ingest) is emitted as if !(cond), so it doesn't print an empty branch.

Tests

tests/jbuilder_conditional_pairs.rs: the three templates above, ingested in memory. It checks the emitted Ruby (every view parses; both branches carry their pair; no io << "" left), then writes the emitted views next to runtime/ruby/json_builder.rb, renders them on CRuby for both widgets with Structs for the rows, and compares with the Rails answers in the table. first_pair is the case that takes the run-time comma. Without the change, 2 of its 3 tests fail (the parse test passes); the render test gets the main column.

Full suite, cargo test --release --no-fail-fast on this machine (fixtures/real-blog generated with bin/rh fixture):

  • main 7fad14c0: 3247 passed, 2 failed, 116 ignored
  • this branch: 3250 passed, 2 failed, 116 ignored (the 3 new tests)

The 2 failures are the same on both (resource_and_unit_batch_helpers_preserve_failures_and_contracts, the_store_fixture_checks_clean).

Sibling PRs that also touch src/lower/jbuilder_to_library/mod.rs: #355, #356, #359. Each is independent of main; whichever lands second rebases. This one moves the pair loop into a function, so its conflicts with the others are the arms they add to that loop.

Found while compiling a Rails API app with --target spinel.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Conditional statements and modifiers in Jbuilder views now preserve their JSON pairs, including nested objects and partials.
    • Generated JSON handles separators correctly across conditional branches and avoids empty appends.
  • Tests
    • Added coverage for Ruby syntax and rendered JSON across conditional branches, modifiers, and first-pair cases.

A statement of an object template that is not a `json.*` call was
Unknown, so a block `if … else … end`, an `unless` and the `if` /
`unless` modifiers became an empty append and the pairs inside them
were dropped.

An `If` statement is now `Cond`: the same `if`, each branch's pairs
appended inside it. The pair loop moves into `emit_pairs`, which threads
a three-state comma (`First`, `After`, `Unknown`) through the
statements and both branches. Only after a conditional whose branches
disagree is the comma decided when the template runs, by asking whether
the accumulator still ends in the object's `{`. Templates without a
conditional emit what they did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
Generated by CodeRabbit — configured

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f8f1b555-867d-4c5a-a344-b01ec6986dbe
📥 Commits

Reviewing files that changed from the base of the PR and between 6e87d30 and b390cbf.

📒 Files selected for processing (2)
  • src/lower/jbuilder_to_library/mod.rs
  • tests/jbuilder_conditional_pairs.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Jbuilder lowering now recognizes conditional statements and modifiers. It emits conditional pairs while tracking comma placement across branches. Integration tests check generated Ruby syntax and verify rendered JSON for conditional branches and modifiers.

Changes

Conditional Jbuilder pairs

Layer / File(s) Summary
Classify and emit conditional pairs
src/lower/jbuilder_to_library/mod.rs
The lowering classifies if expressions as conditional statements and recursively emits branch pairs. Separator state handles first pairs, guaranteed pairs, and branches with differing output. Empty then-branches with nonempty else-branches use a negated condition with swapped branches.
Validate generated conditional views
tests/jbuilder_conditional_pairs.rs
Integration tests cover branch and modifier templates, parse generated Ruby, check for empty append statements, and compare rendered JSON for sized and unsized records.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ExprNodeIf
  participant classify
  participant emit_pairs
  participant RubyView
  ExprNodeIf->>classify: Recognize conditional expression
  classify->>emit_pairs: Pass condition and branches as JbStmt::Cond
  emit_pairs->>emit_pairs: Emit branch pairs with separator state
  emit_pairs->>RubyView: Generate conditional Ruby pairs
Loading

Suggested reviewers: rubys

Merge Risk: ⚪ Minimal · up to b390c

The change emits pairs inside if and unless branches in jbuilder templates, and templates without conditionals keep their previous output. No remaining issues block merging.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: conditional pairs in Jbuilder templates are emitted instead of dropped.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

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