Skip to main content

lib/npm/json.ex

defmodule NPM.JSON do
  @moduledoc """
  JSON helpers for npm manifest and lock files.

  `npm_ex` writes generated JSON files such as `package.json`, `npm.lock`, and
  shrinkwrap files. Those files should be stable across repeated writes, so maps
  are recursively converted to `Jason.OrderedObject` values sorted by key before
  encoding. Jason handles the actual JSON encoding, escaping, duplicate-key
  checks, and pretty formatting.
  """

  @doc "Encode a term as pretty-printed JSON with recursively sorted object keys."
  @spec encode_pretty(term()) :: String.t()
  def encode_pretty(data) do
    data
    |> sort_objects()
    |> Jason.encode!(pretty: true, maps: :strict)
    |> Kernel.<>("\n")
  end

  @doc "Decode JSON into maps with string keys."
  @spec decode(iodata()) :: {:ok, term()} | {:error, Jason.DecodeError.t()}
  def decode(data), do: Jason.decode(data)

  @doc "Decode JSON into maps with string keys, raising on invalid input."
  @spec decode!(iodata()) :: term()
  def decode!(data), do: Jason.decode!(data)

  @doc "Read and decode a JSON file."
  @spec read_file(String.t()) :: {:ok, term()} | {:error, term()}
  def read_file(path) do
    case File.read(path) do
      {:ok, content} -> decode(content)
      error -> error
    end
  end

  defp sort_objects(map) when is_map(map) do
    map
    |> Enum.map(fn {key, value} -> {key, sort_objects(value)} end)
    |> Enum.sort_by(fn {key, _value} -> to_string(key) end)
    |> Jason.OrderedObject.new()
  end

  defp sort_objects(list) when is_list(list), do: Enum.map(list, &sort_objects/1)
  defp sort_objects(value), do: value
end