card form: assert a row field is a hash
edited_values and edited_addresses read a phone, email or address field
with a bare truthiness check, assuming the hash Web#card_form_params
had held it to. CardForm.hash_field now asserts it: none for a form
that rendered no such rows, and a raise for any other shape.
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 0eb56e6..f26159a 100644
--- a/lib/pro_tacts/admin/card_form.rb
+++ b/lib/pro_tacts/admin/card_form.rb
@@ -178,18 +178,16 @@ module ProTacts
# is still a list of one.
#: (Array[Contact::Phone | Contact::Email] rows, String field, String name, VCard vcard, Hash[String, untyped] params) -> VCard
def self.edited_values(rows, field, name, vcard, params)
- edits = params[field]
- if edits
- rows.each do |row|
- 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(row.property)}#{VCard.escape(value)}"],
- )
- end
+ edits = hash_field(params, field)
+ rows.each do |row|
+ 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(row.property)}#{VCard.escape(value)}"],
+ )
end
submitted = Array(params["new_#{field}"]) #: Array[untyped]
@@ -204,20 +202,16 @@ module ProTacts
# one (Admin::ContactsEdit). Removal is the reader's own rule
# (Contact#address_of): a row blank throughout, po box included,
# removes the line, where a partially blanked one keeps it —
- # partial blanks are legal empty components. Every row is a hash,
- # the shape the form posts and Web#card_form_params holds a
- # request to.
+ # partial blanks are legal empty components.
#: (Contact contact, VCard vcard, Hash[String, untyped] params) -> VCard
def self.edited_addresses(contact, vcard, params)
- rows = params["address"]
- if rows
- contact.addresses.each do |address|
- submitted = rows[address.line.digest]
- next if submitted.nil? || address_unchanged?(address, submitted)
-
- line = address_line(address, submitted)
- vcard = vcard.substitute(address.line.digest, line ? [line] : [])
- end
+ rows = hash_field(params, "address")
+ contact.addresses.each do |address|
+ submitted = rows[address.line.digest]
+ next if submitted.nil? || address_unchanged?(address, submitted)
+
+ line = address_line(address, submitted)
+ vcard = vcard.substitute(address.line.digest, line ? [line] : [])
end
# The add dialog names an added address by its add index
@@ -226,10 +220,21 @@ module ProTacts
# after an empty one — so the rows arrive as a hash of hashes
# and read out by their values, several adds in one pass
# included.
- added = params["new_address"] #: Hash[String, Hash[String, untyped]]?
- return vcard if added.nil?
+ added = hash_field(params, "new_address").values #: Array[Hash[String, untyped]]
+ vcard.insert(added.filter_map { address_line(nil, it) })
+ end
- vcard.insert(added.values.filter_map { address_line(nil, it) })
+ # A field the editors post as a hash keyed by row: none where the
+ # form rendered no such rows, and a raise for any other shape.
+ # Web#card_form_params answers that shape as a 400 before a save
+ # gets here, so the raise is the assertion that it did.
+ #: (Hash[String, untyped] params, String key) -> Hash[String, untyped]
+ def self.hash_field(params, key)
+ none = {} #: Hash[String, untyped]
+ value = params.fetch(key, none)
+ raise ArgumentError, "the form's #{key} field is not a hash" unless value.is_a?(Hash)
+
+ value
end
#: (Contact::Address address, Hash[String, untyped] submitted) -> bool
@@ -418,7 +423,7 @@ module ProTacts
"N:#{components.join(";")}"
end
- private_class_method :edited_values, :edited_addresses, :address_unchanged?,
+ private_class_method :edited_values, :edited_addresses, :hash_field, :address_unchanged?,
:address_line, :birthday_component, :text_lines, :edited_text, :edited_fn, :display_name,
:n_line, :company_name, :person_name, :edited_organization
end