Skip to content

[BAC-1482] Add read_only attribute support - #25

Merged
HolyWalley merged 1 commit into
masterfrom
feature/BAC-1482-read-only
Aug 7, 2026
Merged

[BAC-1482] Add read_only attribute support#25
HolyWalley merged 1 commit into
masterfrom
feature/BAC-1482-read-only

Conversation

@HolyWalley

@HolyWalley HolyWalley commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Closes BAC-1482. Blocks BAC-1481.

Supersedes #24, which was closed unmerged. That attempt built request/response schema variants, subtree walking and cycle handling; the ticket has since been rewritten to rule all of that out — "the gem does not need to generate request/response schema variants, strip properties, walk nested entities or handle reference cycles — the OpenAPI consumer does all of that from the annotation alone." This PR is only the annotation. Fresh branch so the diff reads clean; #24's history stays intact for reference.

What

attribute :balance, :entity, class_name: "Entities::Businesses::Balance", read_only: true

emits readOnly: true on that property. 19 lines in the existing schemas/json.rb — no new lib file.

Two things that are load-bearing and not obvious

read_only is stripped before super. ActiveModel::Attributes#attribute forwards **options into Type.lookup, so ActiveModel::Type::String.new(read_only: true) raises ArgumentError at class-definition time. Without the strip the option cannot be declared at all. The interception mirrors the existing one in Meta::Descriptions.

readOnly on a $ref property is wrapped in allOf. In OpenAPI 3.0 a sibling of $ref is ignored, so a bare readOnly: true next to $ref is silently dropped — which is exactly the motivating case (balance is an :entity attribute). make_schema_read_only! mirrors the existing make_schema_nullable!, and the two are idempotent together: nullable + read_only on one attribute yields one allOf carrying both flags.

The duplicated unwrap line between the two methods is deliberate — extracting a shared helper would mean editing make_schema_nullable!, and the copy reads as a mirror of the method directly above it.

Registry

class_attribute :read_only_attributes, written by reassignment (self.read_only_attributes += [...]) rather than mutation. Reassignment creates a subclass-local override, so a subclass declaring a read-only attribute cannot leak it into its parent or a sibling. Mutating the shared default array can — and a mutation test confirmed the inheritance spec is what catches it (swapping += for << fails that example and nothing else).

Coverage

Primitive, :entity, :array-of-entity and :array-of-primitive each verified with and without the flag; plus the $ref wrapping, read_only combined with nullable, and the inheritance guard. Also checked by hand: interaction with enum, desc, default:, untyped attributes, string attribute names, and the inline: true path including nested entities.

Verification

56 examples, 0 failures        (baseline re-confirmed at 50 by stashing)
41 files inspected, no offenses detected

git diff --stat is 2 files changed, 82 insertions(+)zero deletions. That is the mechanical proof for acceptance criterion 2: no pre-existing example, expectation or lib line was edited, including the big equality assertion in schemas/json_spec.rb.

No rubocop:disable, no .rubocop.yml edit, no new dependency, no README change. No version bump — karen pins this gem by git rev.

Not done here

karen-side verification (bump the pinned rev, regenerate swagger/v1/swagger.yaml under RAILS_ENV=test, npm run oapi, diff to nothing with no markers adopted). It was blocked on this branch existing to pin. Worth running before merge.

Unrelated, flagged only: the suite does not load on Ruby 4.0.x — activesupport 7.1.3.4 assumes stdlib gems Ruby 4 ejected. CI is on 3.3.0 and unaffected; run locally with ASDF_RUBY_VERSION=3.3.0 bundle exec rspec.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WnpMsUXxckwovSfTfX37KK

Adds a `read_only: true` option on `attribute`, emitted as `readOnly: true`
on that property in the generated JSON schema:

    attribute :balance, :entity, class_name: "Entities::Businesses::Balance", read_only: true

The option is stripped before `super`, since ActiveModel forwards `**options`
into `Type.lookup` and every symbolic type raises ArgumentError on an unknown
keyword.

`readOnly` on a `$ref` property is wrapped in `allOf`, mirroring the existing
`make_schema_nullable!` -- an OpenAPI 3.0 sibling of `$ref` is ignored, which is
exactly the motivating case. The two are idempotent together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnpMsUXxckwovSfTfX37KK
@HolyWalley
HolyWalley requested a review from taleh007 August 6, 2026 19:47
@HolyWalley
HolyWalley marked this pull request as ready for review August 6, 2026 19:47
@HolyWalley
HolyWalley requested a review from yard August 6, 2026 19:47

@taleh007 taleh007 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.

Legit

@HolyWalley
HolyWalley merged commit c7a86e2 into master Aug 7, 2026
@HolyWalley
HolyWalley deleted the feature/BAC-1482-read-only branch August 7, 2026 10:39
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