Packages

A fast and minimal easy to use user-agent parser.

Current section

Files

Jump to
fast_uaparser lib fast_uaparser.ex
Raw

lib/fast_uaparser.ex

defmodule FastUaparser do
@moduledoc """
Documentation for `FastUaparser`.
"""
alias FastUaparser.{UserAgent, EngineInfo, VersionInfo, OperatingSystem, DeviceInfo}
require Logger
@user_agent_products_regex Regex.compile!("([^/ ]+)/([^ ]+) *(\\([^)]+\\))*")
@ignore_products_as_engine ["Mozilla", "Gecko", "AppleWebKit"]
@doc """
Parses user agent.
"""
@spec parse_ua(binary) :: {:ok, UserAgent.t()} | {:error, any()}
def parse_ua(user_agent) when is_binary(user_agent) do
with splitted <- Regex.scan(@user_agent_products_regex, user_agent),
{:ok, engine_info} <- extract_engine_info(splitted),
{:ok, device_info} <- extract_device_info(splitted) do
{:ok, %UserAgent{engine_info: engine_info, device_info: device_info}}
else
err ->
Logger.error(inspect(err))
{:error, :bad_format}
end
end
@doc """
Extract client engine info from a splitted regex list like:
```elixir
[["Firefox/5.0 (Linux)", "Firefox", "5.0", "Linux"] | _]
```
"""
@spec extract_engine_info(list()) :: {:ok, EngineInfo.t()} | {:error, :bad_format | :empty}
def extract_engine_info(splitted) when is_list(splitted) do
splitted
|> Enum.reduce([], fn
[_full_re, name, version | _extension], acc ->
[{name, version} | acc]
_bad_format, acc ->
acc
end)
|> Enum.reverse()
|> do_extract_engine_info()
end
defp extract_device_info([[_full, _product_name, _version, os_string] | _t]) do
result =
os_string
|> String.replace(~r/[()]/, "")
|> String.split(";")
|> Enum.map(&String.trim/1)
case result do
["Linux", "U", android_version, hardware_info] ->
# Detected Android
with ["Android", version] <- String.split(android_version) do
parts = do_extract_hardware_info_parts(hardware_info)
{:ok,
%DeviceInfo{
manufaturer: nil,
model: List.first(parts),
version: List.last(parts),
os: %OperatingSystem{
family: "Android",
version: parse_version(version)
}
}}
end
[device_type, details] when device_type in ["iPad", "iPod", "iPhone"] ->
# Detected iOS
with ["CPU", device_model, "OS", os_version | _] <- String.split(details) do
{:ok,
%DeviceInfo{
manufaturer: "Apple Inc.",
model: device_model,
os: %OperatingSystem{
family: device_type,
version: parse_version(String.replace(os_version, "_", "."))
}
}}
end
["X11", info] ->
{:ok,
%DeviceInfo{
os: %OperatingSystem{
family: "Linux",
version: "X11 #{info}"
}
}}
_ ->
# invalid input
{:error, :invalid_input}
end
end
defp extract_device_info([_ | tail]), do: extract_device_info(tail)
defp extract_device_info([]), do: {:error, :not_found}
###
# Helpers
###
Enum.each(@ignore_products_as_engine, fn name ->
# To ignore strings like Mozilla and AppleWebKit become the
# detected client engine.
defp do_extract_engine_info([{unquote(name), _} | tail]), do: do_extract_engine_info(tail)
end)
defp do_extract_engine_info([{name, version} | _]) do
{:ok, %EngineInfo{name: name, version: parse_version(version)}}
end
defp do_extract_engine_info([]), do: {:error, :empty}
defp do_extract_engine_info(_), do: {:error, :bad_format}
defp parse_version(version) do
case String.split(version, ".") do
[v] ->
%VersionInfo{code: v}
[major, minor] ->
%VersionInfo{major: major, minor: minor, code: version}
[major, minor, patch] ->
%VersionInfo{major: major, minor: minor, patch: patch, code: version}
[major, minor, patch, patch_minor | _] ->
%VersionInfo{
major: major,
minor: minor,
patch: patch,
patch_minor: patch_minor,
code: version
}
end
end
defp do_extract_hardware_info_parts(hardware_info) do
hardware_info
|> String.reverse()
|> String.split(" ", parts: 2)
|> Enum.map(&String.reverse/1)
|> Enum.reverse()
end
end