vcard: read a parameter by name off the property
Which TYPE a phone meant, and that a name matches without case,
is the grammar's own rule, kept in a caller's private place.
The property answers for its parameters now, the first value returned,
and what stays upstairs is the downcase, the one part Contact earned.
Assisted-by: Claude Opus 5 via Claude Code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WjrPJFvetad8vvCHqjujh
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index 3dd5797..3181354 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -396,12 +396,15 @@ module ProTacts
Address.new(po_box:, extended:, street:, locality:, region:, postal_code:, country:, type: type_of(property), line:)
end
- # RFC 2426 section 3.3.1: TYPE can repeat (TYPE=work;TYPE=voice) or
- # comma-list (TYPE=work,voice) — either arrives here as one
- # parameter per value, so the first is the one this carries.
+ # A row's type parameter (RFC 2426 section 3.3.1), the first of
+ # them where a card lists several — which one that is, and that a
+ # name matches without case, is the grammar's and so
+ # Property#parameter's. What is this layer's is the downcase: the
+ # spelling is the card's, `WORK` and `work` label the same row,
+ # and a screen shows one of them.
#: (VCard::Parser::Property property) -> String?
def type_of(property)
- property.parameters.find { |name, _| name.casecmp?("TYPE") }&.last&.downcase
+ property.parameter("TYPE")&.downcase
end
end
end
diff --git a/lib/pro_tacts/vcard/parser.rb b/lib/pro_tacts/vcard/parser.rb
index c2e15ee..9d6010e 100644
--- a/lib/pro_tacts/vcard/parser.rb
+++ b/lib/pro_tacts/vcard/parser.rb
@@ -89,6 +89,25 @@ module ProTacts
# component instead of ending it.
#: () -> Array[String]
def components = VCard.split_components(value)
+
+ # The named parameter's first value, or nil when the property
+ # carries none. Names compare without case, RFC 2426 section 4
+ # making them case-insensitive, and the first value is the
+ # answer because a parameter can repeat: section 3.3.1's
+ # `TYPE=work;TYPE=voice` and `TYPE=work,voice` are the same
+ # thing and both arrive as two pairs, so a caller wanting all
+ # of them reads `parameters` itself.
+ #
+ # Lookup, not meaning: that a parameter name is
+ # case-insensitive and that a repeat is a second value are
+ # the grammar's rules, which is what keeps this on the shallow
+ # side of the line the class comment draws. Which parameters a
+ # property may carry, and what any of them says, stays the
+ # caller's.
+ #: (String name) -> String?
+ def parameter(name)
+ parameters.find { |parameter, _| parameter.casecmp?(name) }&.last
+ end
end
# One logical line of a card, parsed beside the exact bytes that
diff --git a/test/pro_tacts/vcard/test_parser.rb b/test/pro_tacts/vcard/test_parser.rb
index 3a629a5..23f3ba2 100644
--- a/test/pro_tacts/vcard/test_parser.rb
+++ b/test/pro_tacts/vcard/test_parser.rb
@@ -367,4 +367,23 @@ class VCardParserTest < Minitest::Test
assert_equal "Smith; Jr.;John;;;", property.text
assert_equal ["Smith; Jr.", "John", "", "", ""], property.components
end
+
+ ## Parameters
+
+ # A name matches without case (RFC 2426 section 4), and a parameter
+ # a card lists several values for answers with the first of them —
+ # both spellings of the repeat reaching here as the same two pairs.
+ def test_a_parameter_reads_by_name_without_case
+ property = properties(card("TEL;type=WORK,voice:+1-555-1234")).fetch(0)
+
+ assert_equal "WORK", property.parameter("TYPE")
+ assert_equal "WORK", property.parameter("type")
+ assert_equal ["WORK", "voice"], property.parameters.map(&:last)
+ end
+
+ def test_a_parameter_the_property_does_not_carry_reads_as_nil
+ property = properties(card("TEL:+1-555-1234")).fetch(0)
+
+ assert_nil property.parameter("TYPE")
+ end
end