contact: a phone, email or address row carries its property
CardForm re-read each row's property off its line and raised on a nil,
though Contact#rows builds a row only from a line that read. The rows
now carry that reading, so the save splices it with no check left to
make.
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/card_form.rb b/lib/pro_tacts/admin/card_form.rb
index 728ccf6..0eb56e6 100644
--- a/lib/pro_tacts/admin/card_form.rb
+++ b/lib/pro_tacts/admin/card_form.rb
@@ -181,17 +181,13 @@ module ProTacts
edits = params[field]
if edits
rows.each do |row|
- # These rows read from lines that did (Contact#phones), so
- # a nil is a broken assumption.
- property = row.line.property or raise "a #{field} row read from a line that will not read"
-
submitted = edits[row.line.digest]
next if submitted.nil? || submitted.to_s.strip == row.value.strip
value = submitted.to_s.strip
vcard = vcard.substitute(
row.line.digest,
- value.empty? ? [] : ["#{VCard.header_of(property)}#{VCard.escape(value)}"],
+ value.empty? ? [] : ["#{VCard.header_of(row.property)}#{VCard.escape(value)}"],
)
end
end
@@ -256,7 +252,7 @@ module ProTacts
# header.
#: (Contact::Address? address, Hash[String, untyped] submitted) -> String?
def self.address_line(address, submitted)
- property = address && address.line.property
+ property = address&.property
if property.nil?
components = [""]
else
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index 296d241..9f22592 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -63,14 +63,17 @@ module ProTacts
# (docs/plans/2026-09-18-phone-labels.md, "Reading"), every
# shape's `line` is the parsed line it was read from, the address
# a save names that row by
- # (docs/plans/2026-09-05-web-card-editor.md), and `lent_by` the
+ # (docs/plans/2026-09-05-web-card-editor.md), a repeatable row's
+ # `property` that line's reading, which #rows only builds a row
+ # from, so a save splicing it never meets a line that would not
+ # read, and `lent_by` the
# group lending the row, nil where the card's own (#lent_at).
# Data classes, whose members the inline syntax cannot read; the
# signatures live in sig/pro_tacts/contact.rbs.
# @rbs skip
- Phone = Data.define(:value, :label, :types, :line, :lent_by)
+ Phone = Data.define(:value, :label, :types, :line, :property, :lent_by)
# @rbs skip
- Email = Data.define(:value, :types, :line, :lent_by)
+ Email = Data.define(:value, :types, :line, :property, :lent_by)
# @rbs skip
Note = Data.define(:value, :line)
# @rbs skip
@@ -95,6 +98,7 @@ module ProTacts
:country,
:types,
:line,
+ :property,
:lent_by,
)
@@ -371,6 +375,7 @@ module ProTacts
label: group && labels[group.downcase],
types: property.types - %w[voice],
line:,
+ property:,
lent_by:,
) if value
}
@@ -382,7 +387,7 @@ module ProTacts
value = text_of(property)
# RFC 2426 section 3.3.2: `internet` is the format every address
# has by default, so it names none.
- Email.new(value:, types: property.types - %w[internet], line:, lent_by:) if value
+ Email.new(value:, types: property.types - %w[internet], line:, property:, lent_by:) if value
}
end
@@ -548,15 +553,16 @@ module ProTacts
end
# ADR's components, with a value that is blank throughout reading
- # as no address at all. Carries the line it was read from, the
- # other provenance-carrying shapes' own address for a save.
+ # as no address at all. Carries the line it was read from and its
+ # reading, the other provenance-carrying shapes' own address for a
+ # save.
#: (VCard::Parser::Property property, line: VCard::Parser::Line, lent_by: Group?) -> Address?
def address_of(property, line:, lent_by:)
components = components_of(property)
return if components.none?
po_box, extended, street, locality, region, postal_code, country = components
- Address.new(po_box:, extended:, street:, locality:, region:, postal_code:, country:, types: property.types, line:, lent_by:)
+ Address.new(po_box:, extended:, street:, locality:, region:, postal_code:, country:, types: property.types, line:, property:, lent_by:)
end
end
diff --git a/sig/pro_tacts/contact.rbs b/sig/pro_tacts/contact.rbs
index 93bd045..2af6dc6 100644
--- a/sig/pro_tacts/contact.rbs
+++ b/sig/pro_tacts/contact.rbs
@@ -10,18 +10,20 @@ module ProTacts
attr_reader label: String?
attr_reader types: Array[String]
attr_reader line: VCard::Parser::Line
+ attr_reader property: VCard::Parser::Property
attr_reader lent_by: Group?
- def self.new: (value: String, label: String?, types: Array[String], line: VCard::Parser::Line, lent_by: Group?) -> instance
+ def self.new: (value: String, label: String?, types: Array[String], line: VCard::Parser::Line, property: VCard::Parser::Property, lent_by: Group?) -> instance
end
class Email
attr_reader value: String
attr_reader types: Array[String]
attr_reader line: VCard::Parser::Line
+ attr_reader property: VCard::Parser::Property
attr_reader lent_by: Group?
- def self.new: (value: String, types: Array[String], line: VCard::Parser::Line, lent_by: Group?) -> instance
+ def self.new: (value: String, types: Array[String], line: VCard::Parser::Line, property: VCard::Parser::Property, lent_by: Group?) -> instance
end
class Note
@@ -56,6 +58,7 @@ module ProTacts
attr_reader country: String?
attr_reader types: Array[String]
attr_reader line: VCard::Parser::Line
+ attr_reader property: VCard::Parser::Property
attr_reader lent_by: Group?
def self.new: (
@@ -68,6 +71,7 @@ module ProTacts
country: String?,
types: Array[String],
line: VCard::Parser::Line,
+ property: VCard::Parser::Property,
lent_by: Group?
) -> instance
end