contact: a lent line that will not read raises
composed_note and the contact card skipped a lent NOTE that would not
read, a case every write's own read rules out, and joined a member's
several NOTEs, which migration 012 and Store#assert_one_note rule out.
Contact.lent_text raises for the first, and the member's one NOTE is
read as one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kqo9sKAi4DnKCQkrtYoAwM
diff --git a/lib/pro_tacts/admin/contact_card.rb b/lib/pro_tacts/admin/contact_card.rb
index d1ef160..9a56a03 100644
--- a/lib/pro_tacts/admin/contact_card.rb
+++ b/lib/pro_tacts/admin/contact_card.rb
@@ -119,9 +119,7 @@ module ProTacts
@contact.inherited.each do |lent|
next unless lent.line[/\A[^;:]*/].to_s.casecmp?("NOTE") == true
- text = VCard.new(lent.line).lines.fetch(0).property&.text
- next if text.nil?
-
+ text = Contact.lent_text(lent)
dt(class: "type-label") { "notes" }
dd(class: "type-body-sm") do
div(style: "white-space: pre-wrap;") { text }
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index e1c7762..296d241 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -449,6 +449,17 @@ module ProTacts
@lent_at
end
+ # A lent line's text. Every write that stores one reads it first
+ # (#composed_note's reason), so one that will not read raises
+ # rather than being skipped as though the group lent nothing.
+ #: (Inherited row) -> String
+ def self.lent_text(row)
+ property = VCard.new(row.line).lines.fetch(0).property
+ raise "group #{row.group.id} lends a line that will not read" if property.nil?
+
+ property.text
+ end
+
# Whether a lent line is a NOTE, read off its bytes the way
# EditedContact reads a property name off them: the group schema
# admits no `item1.` prefix to strip (db/migrations/004_groups.rb).
@@ -465,22 +476,17 @@ module ProTacts
# hold no note serves its own NOTE's bytes as they lie, whatever
# they say, the editor's whole safety.
#
- # A member's own two NOTE lines — a card older than the one-note
- # migration, or a bug — join as the member's text, which serves
- # the card this shape means to serve and converges on it at the
- # next write. A lent line that will not read has no text to
- # become and is skipped, unreachable through every write the
- # schema and #shareable? gate.
+ # A member holds at most one NOTE: migration 012 joined the
+ # stored ones and Store#assert_one_note refuses a write of two. A
+ # lent line that will not read is a broken assumption too, every
+ # write the schema and EditedContact#shareable? gate reading it
+ # first, so it raises (Contact.lent_text).
#: (Array[Inherited] rows) -> String?
def composed_note(rows)
return if rows.empty?
- sections = rows.filter_map { |row|
- text = VCard.new(row.line).lines.fetch(0).property&.text
- NoteSections::Section.new(label: row.group.label, text:) if text
- }
- member = @stored.lines.select { it.names?("NOTE") }
- .filter_map { it.property&.text }.join("\n\n")
+ sections = rows.map { NoteSections::Section.new(label: it.group.label, text: Contact.lent_text(it)) }
+ member = @stored.property("NOTE")&.text.to_s
"NOTE:#{VCard.escape(NoteSections.join(member, sections))}"
end
diff --git a/test/pro_tacts/test_contact.rb b/test/pro_tacts/test_contact.rb
index 3ef1a7c..3b45804 100644
--- a/test/pro_tacts/test_contact.rb
+++ b/test/pro_tacts/test_contact.rb
@@ -437,6 +437,15 @@ class ContactTest < Minitest::Test
)
end
+ # Every write that stores a lent line reads it first, so one that
+ # will not read is a broken assumption to raise on rather than a
+ # section to skip as though the group lent nothing.
+ def test_a_lent_note_that_will_not_read_raises
+ error = assert_raises(RuntimeError) { contact(STRUCTURED, inherited: ["NOTE;=;:"]).vcard }
+
+ assert_equal "group nous lends a line that will not read", error.message
+ end
+
# A member with no note of its own serves the section alone, the
# one NOTE inserted before END:VCARD the way a lent line is.
def test_a_member_with_no_note_of_its_own_serves_the_section_alone