From 1d68868d2794cdd30e9592e79be22007add467ff Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 23 Apr 2026 14:23:11 -0500 Subject: [PATCH] refactor: pattern-match InfoLive.Show path parsing + add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parse_path_element_with_link splits its fat `if`/or-chain into three focused helpers: linkable_callsign?/1 (regex + alias check), path_alias?/1 (four String.starts_with guards), and classify_path_element/2 (link vs. text dispatched on the boolean). - display_for_element/2 chooses the callsign form with/without the asterisk via two function heads instead of an inline `if`. Adds 7 tests covering the parse_path_with_links/1 public API — plain callsigns, heard-via asterisk preservation, non-linkable aliases (WIDE/TRACE/RELAY/TCPIP), q-constructs, whitespace trimming, and mixed paths. --- lib/aprsme_web/live/info_live/show.ex | 43 ++++++++++++------- .../live/info_live/show_helpers_test.exs | 43 +++++++++++++++++++ 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/lib/aprsme_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex index c37441e..234024a 100644 --- a/lib/aprsme_web/live/info_live/show.ex +++ b/lib/aprsme_web/live/info_live/show.ex @@ -599,24 +599,35 @@ defmodule AprsmeWeb.InfoLive.Show do defp parse_path_element_with_link(element) do element = String.trim(element) - - # Check if it's a callsign (with or without SSID) - if Regex.match?(~r/^[A-Z0-9]{1,6}(-\d{1,2})?(\*)?$/, element) and - not String.starts_with?(element, "WIDE") and - not String.starts_with?(element, "TRACE") and - not String.starts_with?(element, "RELAY") and - not String.starts_with?(element, "TCPIP") and - not String.starts_with?(element, "q") do - # Remove asterisk if present (indicates the packet was digipeated through this station) - callsign = String.replace(element, "*", "") - display = if String.ends_with?(element, "*"), do: "#{callsign}*", else: callsign - {:link, callsign, display} - else - # Not a linkable callsign, just return as text - {:text, element, element} - end + classify_path_element(linkable_callsign?(element), element) end + # Only real callsigns (not digipeater aliases or q-constructs) become links. + defp linkable_callsign?(element) do + Regex.match?(~r/^[A-Z0-9]{1,6}(-\d{1,2})?(\*)?$/, element) and + not path_alias?(element) + end + + defp path_alias?(element) do + String.starts_with?(element, "WIDE") or + String.starts_with?(element, "TRACE") or + String.starts_with?(element, "RELAY") or + String.starts_with?(element, "TCPIP") or + String.starts_with?(element, "q") + end + + # Strip the trailing `*` from the canonical callsign while preserving it + # in the display string so the UI still shows "was-heard-via" markers. + defp classify_path_element(true, element) do + callsign = String.replace(element, "*", "") + {:link, callsign, display_for_element(String.ends_with?(element, "*"), callsign)} + end + + defp classify_path_element(false, element), do: {:text, element, element} + + defp display_for_element(true, callsign), do: "#{callsign}*" + defp display_for_element(false, callsign), do: callsign + defp decode_path_element(element) do element = String.trim(element) diff --git a/test/aprsme_web/live/info_live/show_helpers_test.exs b/test/aprsme_web/live/info_live/show_helpers_test.exs index b3ef37e..53d500b 100644 --- a/test/aprsme_web/live/info_live/show_helpers_test.exs +++ b/test/aprsme_web/live/info_live/show_helpers_test.exs @@ -99,4 +99,47 @@ defmodule AprsmeWeb.InfoLive.ShowHelpersTest do assert d > 0 end end + + describe "parse_path_with_links/1" do + test "returns empty list for non-binary input" do + assert Show.parse_path_with_links(nil) == [] + assert Show.parse_path_with_links("") == [] + assert Show.parse_path_with_links(123) == [] + end + + test "splits on commas and links plain callsigns" do + result = Show.parse_path_with_links("K5ABC-1,W1XYZ") + assert [{:link, "K5ABC-1", "K5ABC-1"}, {:link, "W1XYZ", "W1XYZ"}] = result + end + + test "preserves the heard-via asterisk in the display text" do + assert [{:link, "K5ABC", "K5ABC*"}] = Show.parse_path_with_links("K5ABC*") + end + + test "does not link WIDE/TRACE/RELAY/TCPIP path aliases" do + assert [{:text, "WIDE1-1", "WIDE1-1"}] = Show.parse_path_with_links("WIDE1-1") + assert [{:text, "TRACE3-3", "TRACE3-3"}] = Show.parse_path_with_links("TRACE3-3") + assert [{:text, "RELAY", "RELAY"}] = Show.parse_path_with_links("RELAY") + assert [{:text, "TCPIP*", "TCPIP*"}] = Show.parse_path_with_links("TCPIP*") + end + + test "does not link q-constructs" do + assert [{:text, "qAC", "qAC"}] = Show.parse_path_with_links("qAC") + assert [{:text, "qAR", "qAR"}] = Show.parse_path_with_links("qAR") + end + + test "trims whitespace before linking" do + assert [{:link, "K5ABC", "K5ABC"}] = Show.parse_path_with_links(" K5ABC ") + end + + test "mixes links and text in a single path" do + result = Show.parse_path_with_links("K5ABC*,WIDE1-1,W1XYZ") + + assert [ + {:link, "K5ABC", "K5ABC*"}, + {:text, "WIDE1-1", "WIDE1-1"}, + {:link, "W1XYZ", "W1XYZ"} + ] = result + end + end end