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, "~> 2.0"}

Documentation is on HexDocs.

Which version do I want?

2.01.9
Elixir~> 1.13 (tested 1.13, 1.16, 1.19, 1.20)~> 1.11 (tested 1.13 through 1.19)
OutputRFC 3986 normalizedas written since 2017
Docsthis fileREADME at v1.9.0 · HexDocs

2.0 requires Elixir 1.13 because it uses URI.new/1. On an older Elixir, pin {:norma, "~> 1.9"} — it is still supported and its output is unchanged from 1.x.

What 2.0 introduces

Full list with before/after strings: Migrating to 2.0 and the CHANGELOG.

Note on compatibility

Norma 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
add_trailing_slash: trueAppends / to the path when the final segment is not file-like
force_root_path: trueReplaces the path with / and clears query + fragment
add_root_path: trueSets path to / only when path is empty
restore_old_query_behavior: true1.x query sort + last-wins dedupe

Host case is always lowercased (RFC 3986 normalize). The 1.x downcase_host option was removed in 2.0 — see Migrating to 2.0.

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")
"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

Every behavior above is pinned by the golden suite. See Migrating to 2.0 for what changed since 1.x.

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.