defmodule NPM.Package.Funding do @moduledoc """ Parses the `funding` field from package.json. Supports all npm funding formats: string URL, object with url/type, and array of funders. """ @doc """ Extracts funding entries from package.json data. """ @spec extract(map()) :: [map()] def extract(%{"funding" => url}) when is_binary(url) do [%{"url" => url, "type" => nil}] end def extract(%{"funding" => %{"url" => _} = funding}) do [funding] end def extract(%{"funding" => funders}) when is_list(funders) do Enum.map(funders, fn url when is_binary(url) -> %{"url" => url, "type" => nil} funder when is_map(funder) -> funder end) end def extract(_), do: [] @doc """ Returns all funding URLs. """ @spec urls(map()) :: [String.t()] def urls(data) do data |> extract() |> Enum.map(& &1["url"]) |> Enum.reject(&is_nil/1) end @doc """ Returns funding types used (github, opencollective, etc.). """ @spec types(map()) :: [String.t()] def types(data) do data |> extract() |> Enum.map(& &1["type"]) |> Enum.reject(&is_nil/1) |> Enum.uniq() end @doc """ Checks if a package has funding info. """ @spec has_funding?(map()) :: boolean() def has_funding?(data), do: extract(data) != [] @doc """ Counts packages with funding across a set. """ @spec funding_stats([map()]) :: map() def funding_stats(packages) do with_funding = Enum.count(packages, &has_funding?/1) all_types = packages |> Enum.flat_map(&types/1) |> Enum.frequencies() |> Enum.sort_by(&elem(&1, 1), :desc) %{ total: length(packages), with_funding: with_funding, without_funding: length(packages) - with_funding, types: all_types } end end