Take an EditedContact apart on asking, not on construction
EditedContact.new now only holds the submission and the contact
before it. EditedContact#decomposed does the birthday split and the
lent-line subtraction and answers the three parts, stored card,
birthday and group edits, which Store#put destructures. It is kept
after the first ask, so the arrival reports go to Sentry once.
The tests assert the whole triple, which also pins the parts a test
used to leave unchecked.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gi55bFkB6wo4jguNtsase4
diff --git a/lib/pro_tacts/edited_contact.rb b/lib/pro_tacts/edited_contact.rb
index 342b0bb..3b06a44 100644
--- a/lib/pro_tacts/edited_contact.rb
+++ b/lib/pro_tacts/edited_contact.rb
@@ -27,9 +27,7 @@ module ProTacts
class EditedContact
# @rbs @before: Contact?
# @rbs @submitted: VCard
- # @rbs @stored: VCard
- # @rbs @birthday: Birthday?
- # @rbs @group_edits: Array[GroupEdit]
+ # @rbs @decomposed: [VCard, Birthday?, Array[GroupEdit]]
# What one classified line asks of the group it came from: the row
# to write, and the line that replaces it — or nil to remove the
@@ -62,24 +60,26 @@ module ProTacts
def initialize(submitted, before:)
@before = before
@submitted = submitted
- @birthday, rest = split_birthday
- @stored, @group_edits = subtract_inherited(rest)
end
attr_reader :before
attr_reader :submitted
- # The card to store: the submission minus its birthday and minus
- # what its groups lend it.
- attr_reader :stored
-
- # The model's new birthday, or nil to empty the model.
- attr_reader :birthday
-
- # The group rows the member's edits rewrite or remove, empty where
- # every lent line came back untouched.
- attr_reader :group_edits
+ # The submission taken back apart: the card to store, minus its
+ # birthday and minus what its groups lend it; the model's new
+ # birthday, or nil to empty the model; and the group rows the
+ # member's edits rewrite or remove, empty where every lent line
+ # came back untouched. Worked out on first ask and kept, so the
+ # arrival reports go to Sentry once however often it is read.
+ #: () -> [VCard, Birthday?, Array[GroupEdit]]
+ def decomposed
+ return @decomposed if defined?(@decomposed)
+
+ birthday, rest = split_birthday
+ stored, group_edits = subtract_inherited(rest)
+ @decomposed = [stored, birthday, group_edits]
+ end
private
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index 3b6eea3..d062457 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -439,7 +439,7 @@ module ProTacts
#
# What is stored is the card minus its birthday and minus what its
# groups lend it, both composed back in on read; EditedContact
- # reads the submission apart and this writes what it answers.
+ # takes the submission apart and this writes the parts.
#
# A card rather than its bytes, so the reading a caller already
# made is the one EditedContact decides from: a PUT has asked
@@ -461,8 +461,7 @@ module ProTacts
# it: the edit, which reads the submission against it, and the
# change log's diff.
before = contact(id)
- edited = EditedContact.new(vcard, before:)
- stored, birthday, edits = edited.stored, edited.birthday, edited.group_edits
+ stored, birthday, edits = EditedContact.new(vcard, before:).decomposed
# The Contact this returns is the composed one — the lent lines
# put back, read off membership as it stands at this write — and
diff --git a/test/pro_tacts/test_edited_contact.rb b/test/pro_tacts/test_edited_contact.rb
index b9b1182..818dccf 100644
--- a/test/pro_tacts/test_edited_contact.rb
+++ b/test/pro_tacts/test_edited_contact.rb
@@ -40,8 +40,10 @@ class EditedContactTest < Minitest::Test
ProTacts::Contact.new(id: "aiden", stored: ProTacts::VCard.new(stored), birthday:, inherited:)
end
- def edit(bytes, before: nil)
- ProTacts::EditedContact.new(ProTacts::VCard.new(bytes), before:)
+ # The parts with the card as its bytes, so a test compares strings.
+ def decompose(bytes, before: nil)
+ stored, birthday, group_edits = ProTacts::EditedContact.new(ProTacts::VCard.new(bytes), before:).decomposed
+ [stored.to_s, birthday, group_edits]
end
def group_edit(line) = ProTacts::EditedContact::GroupEdit.new(group_id: "kkkk", position: 3, line:)
@@ -49,116 +51,96 @@ class EditedContactTest < Minitest::Test
## The birthday
def test_a_modeled_birthday_leaves_the_card_for_the_model
- edited = edit(with_lines("BDAY:1985-04-12"))
-
- assert_equal APRIL_12, edited.birthday
- assert_equal AIDEN, edited.stored.to_s
+ assert_equal [AIDEN, APRIL_12, []], decompose(with_lines("BDAY:1985-04-12"))
assert_empty sentry_messages
end
def test_a_card_without_a_bday_keeps_a_birthday_no_client_was_sent
month_only = ProTacts::Birthday.new(month: 4)
- assert_equal month_only, edit(AIDEN, before: before(birthday: month_only)).birthday
+ assert_equal [AIDEN, month_only, []], decompose(AIDEN, before: before(birthday: month_only))
end
def test_a_card_without_a_bday_drops_a_birthday_the_client_was_sent
- assert_nil edit(AIDEN, before: before(birthday: APRIL_12)).birthday
+ assert_equal [AIDEN, nil, []], decompose(AIDEN, before: before(birthday: APRIL_12))
end
def test_an_unrecognized_bday_stays_in_the_card_and_is_reported
submitted = with_lines("BDAY:1985-13")
- edited = edit(submitted)
- assert_nil edited.birthday
- assert_equal submitted, edited.stored.to_s
+ assert_equal [submitted, nil, []], decompose(submitted)
assert_equal ["a submitted card carried a BDAY line the model does not take"], sentry_messages
end
def test_several_bday_lines_stay_in_the_card_and_are_reported
submitted = with_lines("BDAY:1985-04-12", "BDAY:1986-05-13")
- edited = edit(submitted)
- assert_nil edited.birthday
- assert_equal submitted, edited.stored.to_s
+ assert_equal [submitted, nil, []], decompose(submitted)
assert_equal ["a submitted card carried 2 BDAY lines"], sentry_messages
end
def test_a_write_dropping_a_bday_the_model_did_not_take_is_reported
- edit(AIDEN, before: before(with_lines("BDAY:1985-13")))
+ decompose(AIDEN, before: before(with_lines("BDAY:1985-13")))
assert_equal ["a write dropped 1 BDAY line(s) the model did not take"], sentry_messages
end
+ def test_the_reports_go_once_however_often_it_is_decomposed
+ edited = ProTacts::EditedContact.new(ProTacts::VCard.new(with_lines("BDAY:1985-13")), before: nil)
+ 2.times { edited.decomposed }
+
+ assert_equal 1, sentry_messages.length
+ end
+
## The lent lines
def test_a_contact_in_no_group_is_stored_as_it_arrived
submitted = with_lines(OWN_ADDRESS)
- edited = edit(submitted, before: before)
- assert_equal submitted, edited.stored.to_s
- assert_empty edited.group_edits
+ assert_equal [submitted, nil, []], decompose(submitted, before: before)
end
def test_an_untouched_lent_line_is_subtracted_and_asks_nothing
- edited = edit(with_lines(HOUSEHOLD_ADDRESS), before: before(inherited: LENT))
-
- assert_equal AIDEN, edited.stored.to_s
- assert_empty edited.group_edits
+ assert_equal [AIDEN, nil, []], decompose(with_lines(HOUSEHOLD_ADDRESS), before: before(inherited: LENT))
assert_empty sentry_messages
end
def test_a_reserialized_lent_line_is_still_untouched
- edited = edit(with_lines(RESERIALIZED_ADDRESS), before: before(inherited: LENT))
-
- assert_equal AIDEN, edited.stored.to_s
- assert_empty edited.group_edits
+ assert_equal [AIDEN, nil, []], decompose(with_lines(RESERIALIZED_ADDRESS), before: before(inherited: LENT))
end
def test_a_members_own_line_keeps_its_place_beside_a_lent_one
own = AIDEN.sub("FN:Aiden\r\n", "#{OWN_ADDRESS}\r\nFN:Aiden\r\n")
submitted = own.sub("END:VCARD\r\n", "#{HOUSEHOLD_ADDRESS}\r\nEND:VCARD\r\n")
- assert_equal own, edit(submitted, before: before(own, inherited: LENT)).stored.to_s
+ assert_equal [own, nil, []], decompose(submitted, before: before(own, inherited: LENT))
end
def test_an_edited_lent_line_is_an_edit_of_its_row
- edited = edit(with_lines(EDITED_ADDRESS), before: before(inherited: LENT))
-
- assert_equal AIDEN, edited.stored.to_s
- assert_equal [group_edit(EDITED_ADDRESS)], edited.group_edits
+ assert_equal [AIDEN, nil, [group_edit(EDITED_ADDRESS)]],
+ decompose(with_lines(EDITED_ADDRESS), before: before(inherited: LENT))
end
def test_a_deleted_lent_line_is_a_removal_of_its_row
- edited = edit(AIDEN, before: before(inherited: LENT))
-
- assert_equal AIDEN, edited.stored.to_s
- assert_equal [group_edit(nil)], edited.group_edits
+ assert_equal [AIDEN, nil, [group_edit(nil)]], decompose(AIDEN, before: before(inherited: LENT))
end
def test_two_candidates_for_one_lent_line_stay_in_the_card_and_are_reported
submitted = with_lines(EDITED_ADDRESS, OWN_ADDRESS)
- edited = edit(submitted, before: before(inherited: LENT))
- assert_equal submitted, edited.stored.to_s
- assert_empty edited.group_edits
+ assert_equal [submitted, nil, []], decompose(submitted, before: before(inherited: LENT))
assert_equal 1, sentry_messages.length
end
def test_an_edit_no_group_may_hold_stays_in_the_card_and_is_reported
submitted = with_lines(HOUSEHOLD_ADDRESS.sub("ADR;TYPE=home:", "item1.ADR:"), "item1.X-ABLabel:dom")
- edited = edit(submitted, before: before(inherited: LENT))
- assert_equal submitted, edited.stored.to_s
- assert_empty edited.group_edits
+ assert_equal [submitted, nil, []], decompose(submitted, before: before(inherited: LENT))
assert_equal 1, sentry_messages.length
end
def test_the_birthday_and_the_lent_lines_leave_together
- edited = edit(with_lines(HOUSEHOLD_ADDRESS, "BDAY:1985-04-12"), before: before(inherited: LENT))
-
- assert_equal AIDEN, edited.stored.to_s
- assert_equal APRIL_12, edited.birthday
- assert_empty edited.group_edits
+ assert_equal [AIDEN, APRIL_12, []],
+ decompose(with_lines(HOUSEHOLD_ADDRESS, "BDAY:1985-04-12"), before: before(inherited: LENT))
end
end