aprs.me/lib/aprsme_web/live/shared/packet_handler.ex
Graham McIntire 7db78675a2
refactor: collapse nested if/case into pattern-matched helpers
- SharedPacketHandler.handle_packet_update: dispatch on filter-result,
  and use maybe_enrich/2 dispatched on the enrich? boolean instead of
  an inner `if`. lookup_device_info/1 is now a trio of function heads.
- Cluster.Topology.child_spec: build_spec/3 dispatches on
  (cluster_enabled?, topologies) tuples — no more nested `if`.
- Cluster.PacketDistributor.distribute_packet: maybe_broadcast/2
  dispatches on the leader-and-enabled check; the non-leader / disabled
  paths return :ok explicitly instead of relying on an `if` expression's
  implicit nil.

Adds tests for SharedPacketHandler (filter/enrich/match paths,
callsign_filter + callsign_and_weather_filter factories) and extends
Cluster.Topology tests to cover the nil-topology and opts-merge cases.
Coverage 66.41 → 66.81%.
2026-04-23 13:55:11 -05:00

133 lines
4.2 KiB
Elixir

defmodule AprsmeWeb.Live.SharedPacketHandler do
@moduledoc """
Shared functionality for handling packet updates in LiveView modules.
Provides common patterns for packet filtering, enrichment, and processing.
"""
alias Aprsme.Callsign
alias Aprsme.DeviceCache
alias Aprsme.EncodingUtils
@doc """
Handles incoming packet updates with filtering and processing.
## Options
* `:filter_fn` - Function to determine if packet should be processed (required)
* `:process_fn` - Function to process matching packets and update socket (required)
* `:enrich_packet` - Whether to enrich packet with device info (default: true)
"""
def handle_packet_update(packet, socket, opts) do
filter_fn = Keyword.fetch!(opts, :filter_fn)
process_fn = Keyword.fetch!(opts, :process_fn)
enrich_packet? = Keyword.get(opts, :enrich_packet, true)
dispatch_packet_update(filter_fn.(packet, socket), packet, socket, process_fn, enrich_packet?)
end
defp dispatch_packet_update(false, _packet, socket, _process_fn, _enrich?), do: {:noreply, socket}
defp dispatch_packet_update(true, packet, socket, process_fn, enrich?) do
packet |> maybe_enrich(enrich?) |> process_fn.(socket)
end
defp maybe_enrich(packet, true), do: packet |> sanitize_packet() |> enrich_with_device_info()
defp maybe_enrich(packet, false), do: packet
@doc """
Checks if packet sender matches the given callsign.
"""
def packet_matches_callsign?(packet, callsign) do
packet_sender = Map.get(packet, "sender") || Map.get(packet, :sender, "")
Callsign.matches?(packet_sender, callsign)
end
@doc """
Checks if packet contains weather data.
"""
def has_weather_data?(packet) do
Enum.any?(EncodingUtils.weather_fields(), fn field ->
value = Map.get(packet, field) || Map.get(packet, to_string(field))
not is_nil(value)
end)
end
@doc """
Sanitizes packet strings to handle encoding issues.
"""
def sanitize_packet(packet) do
EncodingUtils.sanitize_packet(packet)
end
@doc """
Enriches packet with device information.
"""
def enrich_with_device_info(packet) do
device_identifier = get_device_identifier(packet)
device = lookup_device_info(device_identifier)
packet
|> Map.put(:device_model, device && device.model)
|> Map.put(:device_vendor, device && device.vendor)
|> Map.put(:device_contact, device && device.contact)
|> Map.put(:device_class, device && device.class)
end
@doc """
Batch enrich multiple packets with device info for better performance.
"""
def enrich_packets_with_device_info(packets) when is_list(packets) do
# Get unique device identifiers from all packets
device_identifiers =
packets
|> Enum.map(&get_device_identifier/1)
|> Enum.reject(&nil_or_empty?/1)
|> Enum.uniq()
# Batch lookup devices (DeviceCache is already optimized with caching)
device_map =
Map.new(device_identifiers, fn identifier -> {identifier, DeviceCache.lookup_device(identifier)} end)
# Enrich packets using the cached device map
Enum.map(packets, fn packet ->
device_identifier = get_device_identifier(packet)
device = Map.get(device_map, device_identifier)
packet
|> Map.put(:device_model, device && device.model)
|> Map.put(:device_vendor, device && device.vendor)
|> Map.put(:device_contact, device && device.contact)
|> Map.put(:device_class, device && device.class)
end)
end
defp get_device_identifier(packet) do
Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier")
end
defp lookup_device_info(nil), do: nil
defp lookup_device_info(""), do: nil
defp lookup_device_info(identifier), do: DeviceCache.lookup_device(identifier)
defp nil_or_empty?(nil), do: true
defp nil_or_empty?(""), do: true
defp nil_or_empty?(_), do: false
@doc """
Creates a filter function that checks both callsign and weather data.
"""
def callsign_and_weather_filter(callsign) do
fn packet, _socket ->
packet_matches_callsign?(packet, callsign) and has_weather_data?(packet)
end
end
@doc """
Creates a filter function that only checks callsign.
"""
def callsign_filter(callsign) do
fn packet, _socket ->
packet_matches_callsign?(packet, callsign)
end
end
end