vcard: callers split a card without rejoining it by hand
The one hand-rolled join left, in carried_and_lost_bday_lines, stays
that way: having insert take Lines would add two Line methods to
remove one call. VCard#broken_assumptions goes, redundant beside
Line#broke_assumption?.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/db/migrations/002_birthdays.rb b/db/migrations/002_birthdays.rb
index dac5f5f..9d2f9e7 100644
--- a/db/migrations/002_birthdays.rb
+++ b/db/migrations/002_birthdays.rb
@@ -42,8 +42,8 @@ Sequel.migration do
self[:cards].order(:id).each do |row|
# The same move a write makes, one arm per shape the stored
# card's BDAY lines take — only the modeled single line moves.
- case ProTacts::VCard.new(row.fetch(:vcard)).lines.partition { it.names?("BDAY") }
- in [[line], others]
+ case ProTacts::VCard.new(row.fetch(:vcard)).extract("BDAY")
+ in [[line], rest]
# Only a line that reads as a BDAY can move it out: the line's
# bytes move as one, and a line holding anything else — or
# nothing this parser will read — stays where it is.
@@ -53,7 +53,7 @@ Sequel.migration do
birthday = ProTacts::Birthday.from_property(property)
next if birthday.nil?
- self[:cards].where(id: row.fetch(:id)).update(vcard: others.map(&:verbatim).join)
+ self[:cards].where(id: row.fetch(:id)).update(vcard: rest.to_s)
self[:birthdays].insert(
card_id: row.fetch(:id),
year: birthday.year,
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index b9ebd11..3903fbc 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -223,12 +223,12 @@ module ProTacts
report_broken_assumptions(card)
existing = birthday_of(id)
birthday, stored =
- case card.lines.partition { it.names?("BDAY") }
- in [[line], others]
+ case card.extract("BDAY")
+ in [[line], rest]
report_unrecognized_bday_lines([line])
property = bday_of(line)
birthday = property && Birthday.from_property(property)
- [birthday, birthday ? others.map(&:verbatim).join : vcard]
+ [birthday, birthday ? rest.to_s : vcard]
in [[], _]
# The rewrite arm: carry the unrendered lines out of the
# stored card, report the unrecognized ones' loss, and keep
@@ -404,7 +404,7 @@ module ProTacts
def carried_and_lost_bday_lines(vcard)
return [[], []] if vcard.nil?
- bdays, = VCard.new(vcard).lines.partition { it.names?("BDAY") }
+ bdays, = VCard.new(vcard).extract("BDAY")
carried, rest = bdays.partition { |line|
property = bday_of(line)
property && Birthday.unrendered_value?(property.value)
@@ -470,7 +470,7 @@ module ProTacts
# raw.
#: (VCard card) -> void
def report_broken_assumptions(card)
- broken = card.broken_assumptions.length
+ broken = card.lines.count { it.broke_assumption? }
return if broken.zero?
Sentry.capture_message(
diff --git a/lib/pro_tacts/vcard.rb b/lib/pro_tacts/vcard.rb
index d39817c..0a891bd 100644
--- a/lib/pro_tacts/vcard.rb
+++ b/lib/pro_tacts/vcard.rb
@@ -188,21 +188,21 @@ module ProTacts
parsed && parsed.find { it.name.casecmp?("UID") }&.value
end
- # The parser assumptions this card broke — the errors themselves,
- # so a caller reporting them can say what was assumed and where.
- # Empty for a card that reads, and for one that merely will not:
- # a broken assumption is news about this server, ordinary bad
- # input is not (Parser::BrokenAssumption). Deciding what to do
- # about it is the caller's — reporting on every parse would fire
- # on every read of the card, not once on its arrival.
- #: () -> Array[Parser::BrokenAssumption]
- def broken_assumptions
- lines.filter_map { |line|
- error = line.error
- error if error.is_a?(Parser::BrokenAssumption)
- }
+ # The lines naming `name`, and the card without them: the split
+ # every caller that moves a property makes, so none of them has to
+ # rejoin the rest by hand and none can lose a byte doing it. The
+ # taken lines come back parsed beside their bytes; what is left
+ # comes back a card, ready to store or to take another split.
+ #: (String name) -> [Array[Line], VCard]
+ def extract(name)
+ taken, rest = lines.partition { it.names?(name) }
+ [taken, VCard.new(rest.map(&:verbatim).join)]
end
+ # The card's bytes, exactly as it was given them.
+ #: () -> String
+ def to_s = @bytes
+
# The card's logical lines, each parsed beside its verbatim bytes —
# the enumeration the questions that move bytes are asked over. A
# continuation travels with its line, terminators attached, so no
diff --git a/test/pro_tacts/vcard/test_lines.rb b/test/pro_tacts/vcard/test_lines.rb
index 9683640..62078e7 100644
--- a/test/pro_tacts/vcard/test_lines.rb
+++ b/test/pro_tacts/vcard/test_lines.rb
@@ -58,14 +58,25 @@ class VCardLinesTest < Minitest::Test
assert_equal CARD, lines.map(&:verbatim).join
end
- def test_partition_splits_a_card_without_losing_a_byte
+ # The split every caller that moves a property makes: the named
+ # lines come back parsed beside their bytes, and what is left comes
+ # back a card, byte for byte, ready to be split again.
+ def test_extract_splits_a_card_without_losing_a_byte
born = CARD.sub("UID:ada\r\n", "UID:ada\r\nBDAY:1985-04-12\r\n")
- bdays, others = VCARD.new(born).lines.partition { it.names?("BDAY") }
+ bdays, rest = VCARD.new(born).extract("BDAY")
assert_equal 1, bdays.length
assert_equal "1985-04-12", bdays.fetch(0).property&.value
assert_equal "BDAY:1985-04-12\r\n", bdays.fetch(0).verbatim
- assert_equal CARD, others.map(&:verbatim).join
+ assert_equal CARD, rest.to_s
+ assert_equal "ada", rest.uid
+ end
+
+ def test_extract_of_a_property_the_card_lacks_leaves_it_whole
+ bdays, rest = VCARD.new(CARD).extract("BDAY")
+
+ assert_empty bdays
+ assert_equal CARD, rest.to_s
end
# A fold travels with its line, byte for byte, and parses as the one
@@ -103,22 +114,18 @@ class VCardLinesTest < Minitest::Test
# in whoever reports it: news about this server and ordinary bad
# input want different handling, and neither caller should have to
# know the parser's error taxonomy to pick.
- def test_a_card_separates_broken_assumptions_from_bad_input
+ def test_a_line_says_whether_it_broke_an_assumption_or_merely_failed
packed = VCARD.new("BEGIN:VCARD\r\nFN:A\rNOTE:n\r\nEND:VCARD\r\n")
- assert_equal 1, packed.broken_assumptions.length
- assert_match "bare CR", packed.broken_assumptions.fetch(0).message
assert_equal [false, true, false], packed.lines.map { it.broke_assumption? }
+ assert_match "bare CR", packed.lines.fetch(1).error&.message
unreadable = VCARD.new("BEGIN:VCARD\r\nBDAY;=;:\r\nEND:VCARD\r\n")
assert_kind_of VCARD::ParseError, unreadable.lines.fetch(1).error
- assert_empty unreadable.broken_assumptions
refute unreadable.lines.fetch(1).broke_assumption?
- end
- def test_a_card_that_reads_broke_no_assumption
- assert_empty VCARD.new(CARD).broken_assumptions
+ refute VCARD.new(CARD).lines.any? { it.broke_assumption? }
end
## insert