defmodule NPM.Package.Keywords do
@moduledoc """
Analyzes package keywords for search and categorization.
Extracts, normalizes, and analyzes keywords from package.json
across installed packages.
"""
@doc """
Extracts keywords from package.json data.
"""
@spec extract(map()) :: [String.t()]
def extract(%{"keywords" => keywords}) when is_list(keywords), do: keywords
def extract(_), do: []
@doc """
Returns the most common keywords across a set of packages.
"""
@spec most_common([map()], non_neg_integer()) :: [{String.t(), non_neg_integer()}]
def most_common(packages, n \\ 10) do
packages
|> Enum.flat_map(&extract/1)
|> Enum.frequencies()
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.take(n)
end
@doc """
Finds packages matching a keyword.
"""
@spec search([{String.t(), map()}], String.t()) :: [String.t()]
def search(packages, keyword) do
keyword_lower = String.downcase(keyword)
packages
|> Enum.filter(fn {_name, data} ->
data
|> extract()
|> Enum.any?(&(String.downcase(&1) == keyword_lower))
end)
|> Enum.map(&elem(&1, 0))
|> Enum.sort()
end
@doc """
Groups packages by keyword.
"""
@spec group_by_keyword([{String.t(), map()}]) :: %{String.t() => [String.t()]}
def group_by_keyword(packages) do
Enum.reduce(packages, %{}, fn {name, data}, acc ->
Enum.reduce(extract(data), acc, fn kw, inner_acc ->
Map.update(inner_acc, kw, [name], &[name | &1])
end)
end)
|> Map.new(fn {k, v} -> {k, Enum.sort(v)} end)
end
@doc """
Returns unique keywords count.
"""
@spec unique_count([map()]) :: non_neg_integer()
def unique_count(packages) do
packages
|> Enum.flat_map(&extract/1)
|> Enum.uniq()
|> length()
end
end