Skip to content

Commit

Permalink
Finalize user agent parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
doomspork committed Nov 19, 2016
1 parent 35d4bc0 commit 203833d
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 32 deletions.
29 changes: 21 additions & 8 deletions lib/user_agent_parser.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
defmodule UserAgentParser do
@moduledoc """
"""

use Application

alias UserAgentParser.Storage
alias UserAgentParser.{Parser, Storage}

@doc false
def start(_type, _args) do
import Supervisor.Spec, warn: false

Expand All @@ -14,12 +18,21 @@ defmodule UserAgentParser do
Supervisor.start_link(children, opts)
end

def parse(user_agent) do
parsed =
user_agent
|> Processor.sanitize
|> Processor.parse!
@doc """
Parse a user-agent string into structs
{:ok, parsed}
end
# Examples
iex> agent_string = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17 Skyfire/2.0"
iex> user_agent = UserAgentParser.parse(agent_string)
iex> to_string(user_agent)
"Skyfire 2.0"
iex> to_string(user_agent.os)
"Mac OS X 10.5.7"
iex> to_string(user_agent.device)
"Other"
"""
def parse(user_agent), do: Parser.parse(pattern, user_agent)

defp pattern, do: Storage.list
end
63 changes: 63 additions & 0 deletions lib/user_agent_parser/parser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
defmodule UserAgentParser.Parser do
@moduledoc """
Handle parsing the user-agent string
"""

alias UserAgentParser.UserAgent, as: Agent
alias UserAgentParser.Parsers.{Device, OperatingSystem, UserAgent}

@doc """
Parse a user-agent string given a set of patterns
"""
def parse({ua_patterns, os_patterns, device_patterns}, user_agent) do
user_agent
|> sanitize
|> parse_os(os_patterns)
|> parse_device(device_patterns)
|> parse_user_agent(ua_patterns)
end

defp find_and_parse(patterns, user_agent, module) do
patterns
|> search(user_agent)
|> module.parse
end

defp match(nil, _string), do: nil
defp match(group, string) do
match =
group
|> Keyword.fetch!(:regex)
|> Regex.run(string)

{group, match}
end

defp parse_device({user_agent, acc}, patterns) do
device = find_and_parse(patterns, user_agent, Device)
{user_agent, Map.put(acc, :device, device)}
end

defp parse_os(user_agent, patterns) do
os = find_and_parse(patterns, user_agent, OperatingSystem)
{user_agent, %{os: os}}
end

defp parse_user_agent({user_agent, acc}, patterns) do
patterns
|> find_and_parse(user_agent, UserAgent)
|> Map.merge(acc)
end

defp sanitize(user_agent), do: String.trim(user_agent)

defp search(groups, string) do
groups
|> Enum.find(fn(group) ->
group
|> Keyword.fetch!(:regex)
|> Regex.match?(string)
end)
|> match(string)
end
end
13 changes: 8 additions & 5 deletions lib/user_agent_parser/user_agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,29 @@ defmodule UserAgentParser.UserAgent do
User Agent struct and helper methods
"""

defstruct [:device, :family, :os, :version]

@doc """
Display the UserAgent as a string
# Examples
iex> version = %UserAgentParser.Version{major: "1", minor: "2", patch: "3", patch_minor: "4"}
iex> agent = %UserAgentParser.UserAgent{family: "Family", version: version}
iex> UserAgentParser.UserAgent.to_string(agent)
iex> to_string(agent)
"Family 1.2.3.4"
iex> agent = %UserAgentParser.UserAgent{family: "Family"}
iex> UserAgentParser.UserAgent.to_string(agent)
iex> to_string(agent)
"Family"
iex> agent = %UserAgentParser.UserAgent{}
iex> UserAgentParser.UserAgent.to_string(agent)
iex> to_string(agent)
"Other"
"""

defstruct [:device, :family, :os, :version]
end

defimpl String.Chars, for: UserAgentParser.UserAgent do
def to_string(%{family: family, version: nil}), do: family_name(family)
def to_string(%{family: family, version: version}), do: "#{family_name(family)} #{version}"

Expand Down
16 changes: 16 additions & 0 deletions test/user_agent_parser/parser_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule UserAgentParser.ParserTest do
use ExUnit.Case

alias UserAgentParser.{Parser, Storage}

@user_agent "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17 Skyfire/2.0"

test "parse a user agent given patterns and a string" do
%{family: family, os: os, version: version} = Parser.parse(Storage.list, @user_agent)

assert family == "Skyfire"
assert to_string(version) == "2.0"
assert os.family == "Mac OS X"
assert to_string(os.version) == "10.5.7"
end
end
2 changes: 1 addition & 1 deletion test/user_agent_parser/parsers/device_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ defmodule UserAgentParser.Parsers.DeviceTest do
{_, _, [pattern|_]} = Storage.list

result = Parser.parse({pattern, ["iPod;", "iPod"]})
assert %Device{family: "iPod"} = result
assert %Device{family: "Spider"} = result
end
end
10 changes: 6 additions & 4 deletions test/user_agent_parser/parsers/operating_system_parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ defmodule UserAgentParser.Parsers.OperatingSystemTest do
use ExUnit.Case

alias UserAgentParser.Parsers.OperatingSystem, as: Parser
alias UserAgentParser.{OperatingSystem, Storage, Version}
alias UserAgentParser.{OperatingSystem, Version}

@user_string "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17 Skyfire/2.0"
@pattern [regex: ~r/((?:Mac ?|; )OS X)[\s\/](?:(\d+)[_.](\d+)(?:[_.](\d+))?|Mach-O)/,
os_replacement: "Mac OS X"]
@match ["Mac OS X 10_5_7", "Mac OS X", "10", "5", "7"]

test "parses operating system information" do
{_, [pattern|_], _} = Storage.list

result = Parser.parse({pattern, ["Mac OS X 10_5_7", "Mac OS X", "10", "5", "7"]})
result = Parser.parse({@pattern, @match})
version = %Version{major: "10", minor: "5", patch: "7"}

assert %OperatingSystem{family: "Mac OS X", version: ^version} = result
end
end
9 changes: 5 additions & 4 deletions test/user_agent_parser/parsers/user_agent_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ defmodule UserAgentParser.Parsers.UserAgentTest do
use ExUnit.Case

alias UserAgentParser.Parsers.UserAgent, as: Parser
alias UserAgentParser.{UserAgent, Storage, Version}
alias UserAgentParser.{UserAgent, Version}

@user_string "Mozilla/5.0 (Windows; U; en-US) AppleWebKit/531.9 (KHTML, like Gecko) AdobeAIR/2.5.1"
@pattern [regex: ~r/\b(MobileIron|Crosswalk|AdobeAIR|FireWeb|Jasmine|ANTGalio|Midori|Fresco|Lobo|PaleMoon|Maxthon|Lynx|OmniWeb|Dillo|Camino|Demeter|Fluid|Fennec|Epiphany|Shiira|Sunrise|Spotify|Flock|Netscape|Lunascape|WebPilot|NetFront|Netfront|Konqueror|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|Opera Mini|iCab|NetNewsWire|ThunderBrowse|Iris|UP\.Browser|Bunjalloo|Google Earth|Raven for Mac|Openwave|MacOutlook)\/(\d+)\.(\d+)\.(\d+)/]
@match ["AdobeAIR/2.5.1", "AdobeAIR", "2", "5", "1"]

test "parses user agent information" do
{[pattern|_], _, _} = Storage.list

result = Parser.parse({pattern, ["AdobeAIR/2.5.1", "AdobeAIR", "2", "5", "1"]})
result = Parser.parse({@pattern, @match})
version = %Version{major: "2", minor: "5", patch: "1"}

assert %UserAgent{family: "AdobeAIR", version: ^version} = result
end
end
12 changes: 6 additions & 6 deletions test/user_agent_parser/parsers/version_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ defmodule UserAgentParser.Parsers.VersionTest do
use ExUnit.Case

alias UserAgentParser.Parsers.Version, as: Parser
alias UserAgentParser.{Version, Storage}

@user_string "Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"
alias UserAgentParser.Version

@user_string "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17 Skyfire/2.0"
@pattern [regex: ~r/((?:Mac ?|; )OS X)[\s\/](?:(\d+)[_.](\d+)(?:[_.](\d+))?|Mach-O)/,
os_replacement: "Mac OS X"]
@match ["Mac OS X", "10", "5", "7"]
@replacement_keys [:not_used, :not_used, :not_used, :not_used]

test "parses device information" do
{_, _, [pattern|_]} = Storage.list

result = Parser.parse({pattern, ["Mac OS X", "10", "5", "7"]}, @replacement_keys)
result = Parser.parse({@pattern, @match}, @replacement_keys)
assert %Version{major: "10", minor: "5", patch: "7"} = result
end
end
4 changes: 0 additions & 4 deletions test/user_agent_parser_test.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
defmodule UserAgentParserTest do
use ExUnit.Case
doctest UserAgentParser

test "the truth" do
assert 1 + 1 == 2
end
end

0 comments on commit 203833d

Please sign in to comment.