Norma

Hex.pmHexdocsDownloadsLicense

Normalize URLs to the format you need.

Installation

Add Norma to your list of dependencies in mix.exs. Tracking the latest release is recommended:

def deps do
[
{:norma, ">= 0.0.0"}
]
end

If you prefer to pin the minor line:

{:norma, "~> 1.9"}

Documentation is on HexDocs.

Note on compatibility

Norma requires Elixir ~> 1.11 and is tested against Elixir 1.13 through 1.19.

It leans heavily on the standard library's URI module, whose parsing behavior has shifted across Elixir releases. If you hit a surprising result, the module's history is usually the fastest explanation.

Usage

Two public functions. Both take a URL string and an optional map of options.

FunctionReturns
Norma.normalize(url, opts \\ %{})String.t(); input it cannot parse as a URL is returned unchanged
Norma.normalize_if_valid(url, opts \\ %{}){:ok, String.t()} or {:error, "Not an URL."}
iex> Norma.normalize("example.com")
"http://example.com"
iex> Norma.normalize_if_valid("example.com")
{:ok, "http://example.com"}
iex> Norma.normalize_if_valid("example")
{:error, "Not an URL."}

Use normalize_if_valid/2 when the input is untrusted — user submissions, scraped text, model output. Use normalize/2 only when the value is already known to be a URL.

Options are a map. Every key defaults to false, and any subset may be combined.

OptionEffect
remove_scheme: trueDrops http:// / https:// from the output
remove_fragment: trueDrops everything from # onward
remove_www: trueDrops a leading www. from the host
downcase_host: trueLowercases the host only; the path keeps its case
add_trailing_slash: trueAppends / to the path when it does not look like a file
force_root_path: trueReplaces the path with /
iex> Norma.normalize("https://example.com", %{remove_scheme: true})
"example.com"
iex> Norma.normalize("https://example.com#faqs", %{remove_fragment: true})
"https://example.com"
iex> Norma.normalize("https://www.example.com", %{remove_www: true})
"https://example.com"
iex> Norma.normalize("https://EXAMPLE.COM/FAQS", %{downcase_host: true})
"https://example.com/FAQS"
iex> Norma.normalize("https://example.com/docs", %{add_trailing_slash: true})
"https://example.com/docs/"
iex> Norma.normalize("https://example.com/docs", %{force_root_path: true})
"https://example.com/"
iex> Norma.normalize("//www.example.com:1337/test#test",
...> %{remove_fragment: true, force_root_path: true, remove_www: true})
"http://example.com:1337/"

Behavior worth knowing before you rely on it

These are the behaviors of the 1.x line, pinned by the test suite as-is. They are queued to change in 2.0.

With Ecto

def creation_changeset(params) do
norma_options = %{
remove_www: true,
force_root_path: true,
remove_fragment: true
}
%MyEntity{}
|> cast(params, @fields)
|> put_change(:url, Norma.normalize(params.url, norma_options))
end

Contributing

Adding options

  1. Add support for the option in /lib/norma/normalizer.ex. Prefer pattern matching and guards over ifs and cases.
  2. Add a test in /test/norma_test.exs.
  3. Add documentation to the README.
  4. Send a PR 🎉

Maintainers


A Mazing project (Studio Closed!)

Originally sponsored by Mazing Studio.