Repository: ua-parser/uap-ruby Branch: main Commit: 38dc9d19771e Files: 28 Total size: 50.4 KB Directory structure: gitextract_ikhkw8zc/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .ruby-version ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin/ │ └── user_agent_parser ├── lib/ │ ├── user_agent_parser/ │ │ ├── cli.rb │ │ ├── device.rb │ │ ├── operating_system.rb │ │ ├── parser.rb │ │ ├── user_agent.rb │ │ └── version.rb │ └── user_agent_parser.rb ├── spec/ │ ├── cli_spec.rb │ ├── custom_regexes.yaml │ ├── device_spec.rb │ ├── operating_system_spec.rb │ ├── other_regexes.yaml │ ├── parser_spec.rb │ ├── spec_helper.rb │ ├── user_agent_spec.rb │ └── version_spec.rb └── user_agent_parser.gemspec ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: push: branches: [main] pull_request: branches: [main] jobs: specs: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: - '3.4' - '3.3' - '3.2' - '3.1' include: - ruby: '3.4' coverage: '1' steps: - uses: actions/checkout@v4 with: submodules: true - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true # 'bundle install' and cache gems - name: Run specs env: SIMPLECOV: ${{ matrix.coverage }} run: | bundle exec rake test ================================================ FILE: .gitignore ================================================ .bundle coverage vendor/bundle .tool-versions pkg ================================================ FILE: .gitmodules ================================================ [submodule "vendor/uap-core"] path = vendor/uap-core url = https://github.com/ua-parser/uap-core.git ================================================ FILE: .ruby-version ================================================ 3.4.4 ================================================ FILE: CHANGELOG.md ================================================ # master # 2.21.0 (2026-02-12) * Sync with https://github.com/ua-parser/uap-core/commit/383604dfd6c7518c152e3bd9b7eda67662b1b343 # 2.20.0 (2025-07-15) * Sync with https://github.com/ua-parser/uap-core/commit/432e95f6767cc8bab4c20c255784cd6f7e93bc15 * drop Ruby 3.0 support * Add Ruby 3.4 support # 2.19.0 (2024-12-10) * Sync with https://github.com/ua-parser/uap-core/commit/d4cde4c565a7e588472fbf6667f01fc4c23fa60b # 2.18.0 (2024-06-04) * Sync with https://github.com/ua-parser/uap-core/commit/df56280c9e2b42dd64be2b750f803c58feb3f94a # 2.17.0 (2024-02-23) * Sync with https://github.com/ua-parser/uap-core/commit/d3450bbe77fe49eb3a234ed6184065260e44d747 # 2.16.0 (2023-06-07) * Sync with https://github.com/ua-parser/uap-core/tree/v0.18.0 # 2.15.0 (2023-04-12) * Expose `parse_os`, `parse_device` and `parse_ua` methods on `Parser` # 2.14.0 (2023-01-31) * Sync with https://github.com/ua-parser/uap-core/commit/1ef0926f2b489cc929589c00f8b8a3efce25acc3 # 2.13.0 (2022-10-21) * Support loading multiple database files (via #70) (@misdoro) * Support `patterns_path` argument but deprecate `pattern_path` attribute accessor in `UserAgentParser::Parser` * Add new `patterns_paths` array argument `UserAgentParser::Parser` to enable loading multiple patterns files # 2.12.0 (2022-10-20) * sync with https://github.com/ua-parser/uap-core/commit/dc85ab2628798538a2874dea4a9563f40a31f55a * Memory optimization (via #104) (@casperisfine) # 2.11.0 (2022-04-18) * Make user agent versions comparable (via #68) (@misdoro) # 2.10.0 (2022-04-18) * sync with uap-core 09e9ccc # 2.9.0 (2022-01-27) * sync with uap-core 0.15.0 # 2.8.0 (2021-11-02) * sync with uap-core 0.14.0 * drop support for ruby 2.4 # 2.7.0 (2020-05-25) * sync with uap-core 0.10.0 ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org/' gemspec group :development, :test do gem 'coveralls_reborn' gem 'minitest' gem 'rake' end ================================================ FILE: MIT-LICENSE ================================================ Copyright (c) 2012 Tim Lucas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # UserAgentParser [![Build Status](https://github.com/ua-parser/uap-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/ua-parser/uap-ruby/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/ua-parser/uap-ruby/badge.svg)](https://coveralls.io/github/ua-parser/uap-ruby) UserAgentParser is a simple, comprehensive Ruby gem for parsing user agent strings. It uses [BrowserScope](http://www.browserscope.org/)'s [parsing patterns](https://github.com/ua-parser/uap-core). ## Supported Rubies * Ruby 3.4 * Ruby 3.3 * Ruby 3.2 * Ruby 3.1 * JRuby ## Installation ```bash $ gem install user_agent_parser ``` ## Example usage ```ruby require 'user_agent_parser' => true user_agent = UserAgentParser.parse 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0;)' => # user_agent.to_s => "IE 9.0" user_agent.family => "IE" user_agent.version.to_s => "9.0" user_agent.version.major => "9" user_agent.version.minor => "0" user_agent.family == "IE" && user_agent.version >= "9" => true operating_system = user_agent.os => # operating_system.to_s => "Windows Vista" # Device information can also be determined from some devices user_agent = UserAgentParser.parse "Mozilla/5.0 (Linux; Android 7.0; SAMSUNG SM-G930T Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/5.0 Chrome/51.0.2704.106 Mobile Safari/537.36" => # user_agent.device.family => "Samsung SM-G930T" user_agent.device.brand => "Samsung" user_agent.device.model => "SM-G930T" user_agent = UserAgentParser.parse "Mozilla/5.0 (iPad; CPU OS 10_2_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) GSA/23.1.148956103 Mobile/14D27 Safari/600.1.4" => # irb(main):026:0> user_agent.device.family => "iPad" irb(main):027:0> user_agent.device.brand => "Apple" irb(main):028:0> user_agent.device.model => "iPad" # The parser database will be loaded and parsed on every call to # UserAgentParser.parse. To avoid this, instantiate your own Parser instance. parser = UserAgentParser::Parser.new parser.parse 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0;)' => # parser.parse 'Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.5.24 Version/10.53' => # ``` In a larger application, you could store a parser in a global to avoid repeat pattern loading: ```ruby module MyApplication # Instantiate the parser on load as it's quite expensive USER_AGENT_PARSER = UserAgentParser::Parser.new def self.user_agent_parser USER_AGENT_PARSER end end ``` ## The pattern database The [ua-parser database](https://github.com/ua-parser/uap-core/blob/master/regexes.yaml) is included via a [git submodule](http://help.github.com/submodules/). To update the database the submodule needs to be updated and the gem re-released (pull requests for this are very welcome!). You can also specify the path to your own, updated and/or customised `regexes.yaml` file as a second argument to `UserAgentParser.parse`: ```ruby UserAgentParser.parse(ua_string, patterns_path: '/some/path/to/regexes.yaml') ``` or when instantiating a `UserAgentParser::Parser`: ```ruby UserAgentParser::Parser.new(patterns_path: '/some/path/to/regexes.yaml').parse(ua_string) ``` Extending the standard database is possible by providing multiple files in `patterns_paths` (plural) array argument: ```ruby UserAgentParser::Parser.new(patterns_paths: [UserAgentParser::DefaultPatternsPath, '/some/path/to/regexes.yaml']) ``` ## Command line tool The gem incldes a `user_agent_parser` bin command which will read from standard input, parse each line and print the result, for example: ```bash $ cat > SOME-FILE-WITH-USER-AGENTS.txt USER_AGENT_1 USER_AGENT_2 ... $ cat SOME-FILE-WITH-USER-AGENTS.txt | user_agent_parser --format '%f %M' | distribution ``` See `user_agent_parser -h` for more information. ## Contributing 1. Fork 2. Hack 3. `rake test` 4. Send a pull request All accepted pull requests will earn you commit and release rights. ## Releasing a new version 1. Update the version in `user_agent_parser.gemspec` 2. `git commit user_agent_parser.gemspec` with the following message format: Version x.x.x Changelog: * Some new feature * Some new bug fix 3. `rake release` 4. Create a [new Github release](https://github.com/ua-parser/uap-ruby/releases/new) ## License MIT ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require 'rake/testtask' require 'bundler' task default: :test desc 'Run tests' Rake::TestTask.new do |t| t.warning = true t.verbose = true t.pattern = 'spec/*_spec.rb' end Bundler::GemHelper.install_tasks # Does not actually get all families, as some are only listed in the regexes, # but gives you a pretty good idea of what will be returned. desc 'Lists all unique family names for browsers and operating systems.' task :families do require 'pathname' require 'pp' root = Pathname(__FILE__).dirname path = root.join('vendor', 'uap-core') browser_families = paths_to_families( [ # path.join('tests', 'test_ua.yaml'), path.join('test_resources', 'firefox_user_agent_strings.yaml'), path.join('test_resources', 'pgts_browser_list.yaml') ] ) os_families = paths_to_families( [ # path.join('tests', 'test_os.yaml'), path.join('test_resources', 'additional_os_tests.yaml') ] ) device_families = paths_to_families( [ # path.join('tests', 'test_device.yaml'), ] ) puts "\n\nBrowser Families" puts browser_families.inspect puts "\n\nOS Families" puts os_families.inspect puts "\n\nDevice Families" puts device_families.inspect puts "\n\n" puts "Browser Family Count: #{browser_families.size}" puts "OS Family Count: #{os_families.size}" puts "Device Family Count: #{device_families.size}" end def paths_to_families(paths) require 'yaml' families = [] paths.each do |path| data = YAML.load_file(path) test_cases = data.fetch('test_cases') families.concat test_cases.map { |row| row['family'] } end families.compact.uniq.sort end ================================================ FILE: bin/user_agent_parser ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require 'optparse' require 'user_agent_parser' require 'user_agent_parser/cli' options = {} optparse = OptionParser.new do |opts| opts.on('--family', 'Print family only') do options[:family] = true end opts.on('--name', 'Print name (alias for family) only') do options[:family] = true end opts.on('--version', 'Print version only') do options[:version] = true end opts.on('--major', 'Print major version only') do options[:major] = true end opts.on('--minor', 'Print minor version only') do options[:minor] = true end opts.on('--os', 'Print operating system only') do options[:os] = true end opts.on('--format format', 'Print output in specified format. The available formatters are:', ' - %f: family', ' - %n: name (alias for family)', ' - %v: version', ' - %M: major version', ' - %m: minor version', ' - %o: operating system' ) do |format| options[:format] = format end opts.on('-h', '--help', 'Display this screen') do puts opts exit end end optparse.parse! parser = UserAgentParser::Parser.new ARGF.each do |line| puts UserAgentParser::Cli.new(parser.parse(line), options).run! end ================================================ FILE: lib/user_agent_parser/cli.rb ================================================ # frozen_string_literal: true module UserAgentParser class Cli def initialize(user_agent, options = {}) @user_agent = user_agent @options = options end def run! if @options[:family] @user_agent.family elsif @options[:name] @user_agent.name elsif @options[:version] with_version(&:to_s) elsif @options[:major] major elsif @options[:minor] minor elsif @options[:os] @user_agent.os.to_s elsif (format = @options[:format]) format .gsub('%f', @user_agent.family) .gsub('%n', @user_agent.name) .gsub('%v', version.to_s) .gsub('%M', major.to_s) .gsub('%m', minor.to_s) .gsub('%o', @user_agent.os.to_s) else @user_agent.to_s end end private def major with_version(&:major) end def minor with_version(&:minor) end def version @version ||= @user_agent.version end def with_version yield(version) if version end end end ================================================ FILE: lib/user_agent_parser/device.rb ================================================ # frozen_string_literal: true module UserAgentParser class Device DEFAULT_FAMILY = 'Other' attr_reader :family, :model, :brand alias name family def initialize(family = nil, model = nil, brand = nil) @family = family || DEFAULT_FAMILY @model = model || @family @brand = brand end def to_s family end def inspect "#<#{self.class} #{self}>" end def eql?(other) self.class.eql?(other.class) && family == other.family end alias == eql? def to_h { family: family, model: model, brand: brand } end end end ================================================ FILE: lib/user_agent_parser/operating_system.rb ================================================ # frozen_string_literal: true module UserAgentParser class OperatingSystem DEFAULT_FAMILY = 'Other' attr_reader :family, :version alias name family def initialize(family = DEFAULT_FAMILY, version = nil) @family = family @version = version end def to_s string = family string += " #{version}" unless version.nil? string end def inspect "#<#{self.class} #{self}>" end def eql?(other) self.class.eql?(other.class) && family == other.family && version == other.version end alias == eql? def to_h { version: version.to_h, family: family } end end end ================================================ FILE: lib/user_agent_parser/parser.rb ================================================ # frozen_string_literal: true require 'yaml' module UserAgentParser class Parser extend Gem::Deprecate FAMILY_REPLACEMENT_KEYS = %w[ family_replacement v1_replacement v2_replacement v3_replacement v4_replacement ].freeze OS_REPLACEMENT_KEYS = %w[ os_replacement os_v1_replacement os_v2_replacement os_v3_replacement os_v4_replacement ].freeze private_constant :FAMILY_REPLACEMENT_KEYS, :OS_REPLACEMENT_KEYS attr_reader :patterns_paths def initialize(patterns_path: nil, patterns_paths: []) @patterns_paths = [patterns_path, *patterns_paths].compact @patterns_paths = [UserAgentParser::DefaultPatternsPath] if @patterns_paths.empty? @ua_patterns, @os_patterns, @device_patterns = load_patterns(@patterns_paths) end def parse(user_agent) os = parse_os(user_agent) device = parse_device(user_agent) parse_ua(user_agent, os, device) end def parse_os(user_agent) pattern, match = first_pattern_match(@os_patterns, user_agent) if match os_from_pattern_match(pattern, match) else OperatingSystem.new end end def parse_device(user_agent) pattern, match = first_pattern_match(@device_patterns, user_agent) if match device_from_pattern_match(pattern, match) else Device.new end end def parse_ua(user_agent, os = nil, device = nil) pattern, match = first_pattern_match(@ua_patterns, user_agent) if match user_agent_from_pattern_match(pattern, match, os, device) else UserAgent.new(nil, nil, os, device) end end def patterns_path patterns_paths.first end deprecate :patterns_path, :patterns_paths, 2022, 12 private def load_patterns(patterns_paths) patterns_paths.each_with_object([[], [], []]) do |path, patterns| ua_patterns, os_patterns, device_patterns = load_patterns_file(path) patterns[0] += ua_patterns patterns[1] += os_patterns patterns[2] += device_patterns end end def load_patterns_file(path) yml = begin YAML.load_file(path, freeze: true) rescue ArgumentError YAML.load_file(path) end [ parse_pattern(yml['user_agent_parsers']), parse_pattern(yml['os_parsers']), parse_pattern(yml['device_parsers']), ] end def parse_pattern(patterns) patterns.map do |pattern| pattern = pattern.dup pattern[:regex] = Regexp.new(pattern.delete('regex'), pattern.delete('regex_flag') == 'i') pattern end end def first_pattern_match(patterns, value) patterns.each do |pattern| return [pattern, pattern[:regex].match(value)] if pattern[:regex].match?(value) end nil end def user_agent_from_pattern_match(pattern, match, os = nil, device = nil) family, *versions = from_pattern_match(FAMILY_REPLACEMENT_KEYS, pattern, match) UserAgent.new(family, version_from_segments(*versions), os, device) end def os_from_pattern_match(pattern, match) os, *versions = from_pattern_match(OS_REPLACEMENT_KEYS, pattern, match) OperatingSystem.new(os, version_from_segments(*versions)) end def device_from_pattern_match(pattern, match) match = match.to_a.map(&:to_s) family = model = match[1] brand = nil if pattern['device_replacement'] family = pattern['device_replacement'] match.each_with_index { |m, i| family = family.sub("$#{i}", m) } end if pattern['model_replacement'] model = pattern['model_replacement'] match.each_with_index { |m, i| model = model.sub("$#{i}", m) } end if pattern['brand_replacement'] brand = pattern['brand_replacement'] match.each_with_index { |m, i| brand = brand.sub("$#{i}", m) } brand.strip! end model&.strip! Device.new(family.strip, model, brand) end # Maps replacement keys to their values def from_pattern_match(keys, pattern, match) keys.each_with_index.map do |key, idx| # Check if there is any replacement specified if pattern[key] interpolate(pattern[key], match) else # No replacement defined, just return correct match group match[idx + 1] end end end # Interpolates a string with data from matches if specified def interpolate(replacement, match) group_idx = replacement.index('$') return replacement if group_idx.nil? group_nbr = replacement[group_idx + 1] replacement.sub("$#{group_nbr}", match[group_nbr.to_i]) end def version_from_segments(*segments) return if segments.all?(&:nil?) Version.new(*segments) end end end ================================================ FILE: lib/user_agent_parser/user_agent.rb ================================================ # frozen_string_literal: true module UserAgentParser class UserAgent DEFAULT_FAMILY = 'Other' attr_reader :family, :version, :os, :device alias name family def initialize(family = nil, version = nil, os = nil, device = nil) @family = family || DEFAULT_FAMILY @version = version @os = os @device = device end def to_s string = family string += " #{version}" if version string end def inspect string = to_s string += " (#{os})" if os string += " (#{device})" if device "#<#{self.class} #{string}>" end def eql?(other) self.class.eql?(other.class) && family == other.family && version == other.version && os == other.os end alias == eql? def to_h { device: device.to_h, family: family, os: os.to_h, version: version.to_h } end end end ================================================ FILE: lib/user_agent_parser/version.rb ================================================ # frozen_string_literal: true require 'rubygems/version' module UserAgentParser class Version include Comparable # Private: Regex used to split string version string into major, minor, # patch, and patch_minor. SEGMENTS_REGEX = /\d+\-\d+|\d+[a-zA-Z]+$|\d+|[A-Za-z][0-9A-Za-z-]*$/.freeze attr_reader :version alias to_s version def initialize(*args) # If only one string argument is given, assume a complete version string # and attempt to parse it if args.length == 1 && args.first.is_a?(String) @version = args.first.to_s.strip else @segments = args.compact.map(&:to_s).map(&:strip) @version = segments.join('.') end end def major segments[0] end def minor segments[1] end def patch segments[2] end def patch_minor segments[3] end def inspect "#<#{self.class} #{self}>" end def eql?(other) self.class.eql?(other.class) && version == other.version end def <=>(other) Gem::Version.new(version).<=>(Gem::Version.new(other.to_s)) end def segments @segments ||= version.scan(SEGMENTS_REGEX) end def to_h { version: version, major: major, minor: minor, patch: patch, patch_minor: patch_minor } end end end ================================================ FILE: lib/user_agent_parser.rb ================================================ # frozen_string_literal: true require 'user_agent_parser/parser' require 'user_agent_parser/user_agent' require 'user_agent_parser/version' require 'user_agent_parser/operating_system' require 'user_agent_parser/device' module UserAgentParser DefaultPatternsPath = File.join(File.dirname(__FILE__), '../vendor/uap-core/regexes.yaml') # Parse the given +user_agent_string+, returning a +UserAgent+ def self.parse(user_agent_string, **args) Parser.new(**args).parse(user_agent_string) end end ================================================ FILE: spec/cli_spec.rb ================================================ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + '/spec_helper') require 'user_agent_parser/cli' describe UserAgentParser::Cli do let(:cli) { UserAgentParser::Cli.new(user_agent, options) } let(:options) { {} } let(:parser) { UserAgentParser::Parser.new } let(:user_agent) do parser.parse('Mozilla/5.0 (iPad; CPU OS 6_0_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A523 Safari/8536.25') end it 'prints family and version when no options' do _(cli.run!).must_equal('Mobile Safari 6.0') end describe 'invalid version' do let(:user_agent) do parser.parse('Mozilla/5.0 (iPad; CPU OS like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/XYZ Mobile/10A523 Safari/8536.25') end describe '--version' do let(:options) { { version: true } } it 'returns nil' do _(cli.run!).must_be_nil end end describe '--major' do let(:options) { { major: true } } it 'returns nil' do _(cli.run!).must_be_nil end end describe '--minor' do let(:options) { { minor: true } } it 'returns nil' do _(cli.run!).must_be_nil end end describe '--format' do let(:options) { { format: '%n|%f|%v|%M|%m|%o' } } it 'returns string without versions' do _(cli.run!).must_equal('Mobile Safari|Mobile Safari||||iOS') end end end describe '--name' do let(:options) { { name: true } } it 'returns name only' do _(cli.run!).must_equal('Mobile Safari') end end describe '--family' do let(:options) { { family: true } } it 'returns family only' do _(cli.run!).must_equal('Mobile Safari') end end describe '--version' do let(:options) { { version: true } } it 'returns version only' do _(cli.run!).must_equal('6.0') end end describe '--major' do let(:options) { { major: true } } it 'returns major version only' do _(cli.run!).must_equal('6') end end describe '--minor' do let(:options) { { minor: true } } it 'returns minor version only' do _(cli.run!).must_equal('0') end end describe '--os' do let(:options) { { os: true } } it 'returns operating system only' do _(cli.run!).must_equal('iOS 6.0.1') end end describe '--format' do let(:options) { { format: '%n|%v|%M|%m|%o' } } it 'return string with correct replacements' do _(cli.run!).must_equal('Mobile Safari|6.0|6|0|iOS 6.0.1') end end end ================================================ FILE: spec/custom_regexes.yaml ================================================ user_agent_parsers: - regex: 'Any.*' family_replacement: 'Custom browser' v1_replacement: '1' v2_replacement: '2' v3_replacement: '3' v4_replacement: '4' os_parsers: - regex: 'Any.*' os_replacement: 'Custom OS' os_v1_replacement: '1' os_v2_replacement: '2' device_parsers: - regex: 'Any.*' device_replacement: 'Custom device' ================================================ FILE: spec/device_spec.rb ================================================ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe UserAgentParser::Device do describe '#name' do it 'returns family' do os = UserAgentParser::Device.new('iPod') _(os.name).must_equal os.family end end describe '#to_s' do it 'returns a string of just the family' do os = UserAgentParser::Device.new('iPod') _(os.to_s).must_equal 'iPod' end end describe '#==' do it 'returns true for same family' do device1 = UserAgentParser::Device.new('iPod') device2 = UserAgentParser::Device.new('iPod') _(device1).must_equal device2 end it 'returns false different family' do device1 = UserAgentParser::Device.new('iPod') device2 = UserAgentParser::Device.new('iPad') _(device1).wont_equal device2 end end describe '#eql?' do it 'returns true for same family' do device1 = UserAgentParser::Device.new('iPod') device2 = UserAgentParser::Device.new('iPod') assert_equal true, device1.eql?(device2) end it 'returns false different family' do device1 = UserAgentParser::Device.new('iPod') device2 = UserAgentParser::Device.new('iPad') assert_equal false, device1.eql?(device2) end end describe '#inspect' do it 'returns class family and instance to_s' do device = UserAgentParser::Device.new('iPod') _(device.inspect.to_s).must_equal '#' end end end ================================================ FILE: spec/operating_system_spec.rb ================================================ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe UserAgentParser::OperatingSystem do describe '#name' do it 'returns family' do os = UserAgentParser::OperatingSystem.new('Windows') _(os.name).must_equal os.family end end describe '#to_s' do it 'returns a string of just the family' do os = UserAgentParser::OperatingSystem.new('Windows') _(os.to_s).must_equal 'Windows' end it 'returns a string of family and version' do version = UserAgentParser::Version.new('7') os = UserAgentParser::OperatingSystem.new('Windows', version) _(os.to_s).must_equal 'Windows 7' end end describe '#==' do it "returns true for same user agents across different O/S's" do version = UserAgentParser::Version.new('7') os1 = UserAgentParser::OperatingSystem.new('Windows', version) os2 = UserAgentParser::OperatingSystem.new('Windows', version) _(os1).must_equal os2 end it 'returns false for same family, different versions' do seven = UserAgentParser::Version.new('7') eight = UserAgentParser::Version.new('8') os1 = UserAgentParser::OperatingSystem.new('Windows', seven) os2 = UserAgentParser::OperatingSystem.new('Windows', eight) _(os1).wont_equal os2 end it 'returns false for different family, same version' do version = UserAgentParser::Version.new('7') os1 = UserAgentParser::OperatingSystem.new('Windows', version) os2 = UserAgentParser::OperatingSystem.new('Blah', version) _(os1).wont_equal os2 end end describe '#eql?' do it "returns true for same user agents across different O/S's" do version = UserAgentParser::Version.new('7') os1 = UserAgentParser::OperatingSystem.new('Windows', version) os2 = UserAgentParser::OperatingSystem.new('Windows', version) assert_equal true, os1.eql?(os2) end it 'returns false for same family, different versions' do seven = UserAgentParser::Version.new('7') eight = UserAgentParser::Version.new('8') os1 = UserAgentParser::OperatingSystem.new('Windows', seven) os2 = UserAgentParser::OperatingSystem.new('Windows', eight) assert_equal false, os1.eql?(os2) end it 'returns false for different family, same version' do version = UserAgentParser::Version.new('7') os1 = UserAgentParser::OperatingSystem.new('Windows', version) os2 = UserAgentParser::OperatingSystem.new('Blah', version) assert_equal false, os1.eql?(os2) end end describe '#inspect' do it 'returns class family and instance to_s' do version = UserAgentParser::Version.new('10.7.4') os = UserAgentParser::OperatingSystem.new('OS X', version) _(os.inspect.to_s).must_equal '#' end end end ================================================ FILE: spec/other_regexes.yaml ================================================ user_agent_parsers: - regex: 'Other.*' family_replacement: 'Other browser' v1_replacement: '1' v2_replacement: '2' v3_replacement: '3' v4_replacement: '4' os_parsers: - regex: 'Other.*' os_replacement: 'Other OS' os_v1_replacement: '1' os_v2_replacement: '2' device_parsers: - regex: 'Other.*' device_replacement: 'Other device' ================================================ FILE: spec/parser_spec.rb ================================================ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + '/spec_helper') require 'yaml' describe UserAgentParser::Parser do PARSER = UserAgentParser::Parser.new # Some Ruby versions (JRuby) need sanitised test names, as some chars screw # up the test method definitions def self.test_case_to_test_name(test_case) name = "#{test_case['user_agent_string']}_#{test_case['family']}" name.gsub(/[^a-z0-9_.-]/i, '_').squeeze('_') end def self.file_to_test_cases(file) file_to_yaml(file)['test_cases'].map do |test_case| { 'user_agent_string' => test_case['user_agent_string'], 'family' => test_case['family'], 'major' => test_case['major'], 'minor' => test_case['minor'], 'patch' => test_case['patch'], 'patch_minor' => test_case['patch_minor'], 'brand' => test_case['brand'], 'model' => test_case['model'] } end.reject do |test_case| # We don't do the hacky javascript user agent overrides test_case.key?('js_ua') || test_case['family'] == 'IE Platform Preview' || test_case['user_agent_string'].include?('chromeframe;') end end def self.file_to_yaml(resource) uap_path = File.expand_path('../../vendor/uap-core', __FILE__) resource_path = File.join(uap_path, resource) YAML.load_file(resource_path) end def self.user_agent_test_cases file_to_test_cases('test_resources/firefox_user_agent_strings.yaml') file_to_test_cases('tests/test_ua.yaml') end def self.operating_system_test_cases file_to_test_cases('tests/test_os.yaml') + file_to_test_cases('test_resources/additional_os_tests.yaml') end def self.device_test_cases file_to_test_cases('tests/test_device.yaml') end def custom_patterns_path File.join(File.dirname(__FILE__), 'custom_regexes.yaml') end def other_patterns_path File.join(File.dirname(__FILE__), 'other_regexes.yaml') end describe '::parse' do it 'parses a UA' do ua = UserAgentParser.parse('Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3') _(ua.family).must_equal('Safari') end it 'accepts a custom patterns path' do ua = UserAgentParser.parse('Any user agent string', patterns_path: custom_patterns_path) _(ua.family).must_equal('Custom browser') end end describe '#initialize with a custom patterns path' do it 'accepts a single patterns_path string' do parser = UserAgentParser::Parser.new(patterns_path: custom_patterns_path) ua = parser.parse('Any user agent string') _(parser.patterns_paths).must_equal([custom_patterns_path]) _(parser.patterns_path).must_equal(custom_patterns_path) _(ua.family).must_equal('Custom browser') _(ua.version.major).must_equal('1') _(ua.version.minor).must_equal('2') _(ua.version.patch).must_equal('3') _(ua.version.patch_minor).must_equal('4') _(ua.os.family).must_equal('Custom OS') _(ua.os.version.major).must_equal('1') _(ua.os.version.minor).must_equal('2') _(ua.device.family).must_equal('Custom device') end it 'accepts patterns_paths array' do patterns_paths = [custom_patterns_path, other_patterns_path] parser = UserAgentParser::Parser.new(patterns_paths: patterns_paths) _(parser.patterns_paths).must_equal(patterns_paths) _(parser.patterns_path).must_equal(custom_patterns_path) ua = parser.parse('Any user agent string') oua = parser.parse('Other user agent string') _(ua.family).must_equal('Custom browser') _(oua.family).must_equal('Other browser') end end describe '#parse' do user_agent_test_cases.each do |test_case| it "parses UA for #{test_case_to_test_name(test_case)}" do user_agent = PARSER.parse(test_case['user_agent_string']) if test_case['family'] _(user_agent.family).must_equal_test_case_property(test_case, 'family') end if test_case['major'] _(user_agent.version.major).must_equal_test_case_property(test_case, 'major') end if test_case['minor'] _(user_agent.version.minor).must_equal_test_case_property(test_case, 'minor') end if test_case['patch'] _(user_agent.version.patch).must_equal_test_case_property(test_case, 'patch') end end end operating_system_test_cases.each do |test_case| it "parses OS for #{test_case_to_test_name(test_case)}" do user_agent = PARSER.parse(test_case['user_agent_string']) operating_system = user_agent.os if test_case['family'] _(operating_system.family).must_equal_test_case_property(test_case, 'family') end if test_case['major'] _(operating_system.version.major).must_equal_test_case_property(test_case, 'major') end if test_case['minor'] _(operating_system.version.minor).must_equal_test_case_property(test_case, 'minor') end if test_case['patch'] _(operating_system.version.patch).must_equal_test_case_property(test_case, 'patch') end if test_case['patch_minor'] _(operating_system.version.patch_minor).must_equal_test_case_property(test_case, 'patch_minor') end end end device_test_cases.each do |test_case| it "parses device for #{test_case_to_test_name(test_case)}" do user_agent = PARSER.parse(test_case['user_agent_string']) device = user_agent.device if test_case['family'] _(device.family).must_equal_test_case_property(test_case, 'family') end if test_case['model'] _(device.model).must_equal_test_case_property(test_case, 'model') end if test_case['brand'] _(device.brand).must_equal_test_case_property(test_case, 'brand') end end end end describe '#parse_os' do operating_system_test_cases.each do |test_case| it "parses OS for #{test_case_to_test_name(test_case)}" do operating_system = PARSER.parse_os(test_case['user_agent_string']) if test_case['family'] _(operating_system.family).must_equal_test_case_property(test_case, 'family') end if test_case['major'] _(operating_system.version.major).must_equal_test_case_property(test_case, 'major') end if test_case['minor'] _(operating_system.version.minor).must_equal_test_case_property(test_case, 'minor') end if test_case['patch'] _(operating_system.version.patch).must_equal_test_case_property(test_case, 'patch') end if test_case['patch_minor'] _(operating_system.version.patch_minor).must_equal_test_case_property(test_case, 'patch_minor') end end end end describe '#parse_device' do device_test_cases.each do |test_case| it "parses device for #{test_case_to_test_name(test_case)}" do device = PARSER.parse_device(test_case['user_agent_string']) if test_case['family'] _(device.family).must_equal_test_case_property(test_case, 'family') end if test_case['model'] _(device.model).must_equal_test_case_property(test_case, 'model') end if test_case['brand'] _(device.brand).must_equal_test_case_property(test_case, 'brand') end end end end describe '#parse_ua' do user_agent_test_cases.each do |test_case| it "parses UA for #{test_case_to_test_name(test_case)}" do user_agent = PARSER.parse_ua(test_case['user_agent_string']) assert_nil user_agent.os assert_nil user_agent.device if test_case['family'] _(user_agent.family).must_equal_test_case_property(test_case, 'family') end if test_case['major'] _(user_agent.version.major).must_equal_test_case_property(test_case, 'major') end if test_case['minor'] _(user_agent.version.minor).must_equal_test_case_property(test_case, 'minor') end if test_case['patch'] _(user_agent.version.patch).must_equal_test_case_property(test_case, 'patch') end end end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true if ENV['SIMPLECOV'] == '1' require 'coveralls' require 'simplecov' SimpleCov.formatter = Coveralls::SimpleCov::Formatter SimpleCov.start do add_filter '/.bundle/' add_filter '/doc/' add_filter '/spec/' add_filter '/config/' merge_timeout 600 end end require 'minitest/autorun' $:.unshift File.expand_path('../../lib', __FILE__) require 'user_agent_parser' module Minitest module Assertions # Asserts the test case property is equal to the expected value. On failure # the message includes the property and user_agent_string from the test # case for easier debugging def assert_test_case_property_equal(test_case, actual, test_case_property) assert_equal test_case[test_case_property], actual, "#{test_case_property} failed for user agent: #{test_case['user_agent_string']}" end Object.infect_an_assertion :assert_test_case_property_equal, :must_equal_test_case_property end end ================================================ FILE: spec/user_agent_spec.rb ================================================ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe UserAgentParser::UserAgent do describe '#to_s' do it 'returns a string of just the family' do _(UserAgentParser::UserAgent.new('Chrome').to_s).must_equal 'Chrome' end it 'returns a string of family and version' do version = UserAgentParser::Version.new('1.2.3pre') agent = UserAgentParser::UserAgent.new('Chrome', version) _(agent.to_s).must_equal 'Chrome 1.2.3pre' end end describe '#initialize' do describe 'with family' do it 'sets family' do agent = UserAgentParser::UserAgent.new('Chromium') _(agent.family).must_equal 'Chromium' end end describe 'with no family' do it 'sets family to Other' do agent = UserAgentParser::UserAgent.new _(agent.family).must_equal 'Other' end end describe 'with version' do it 'sets version' do version = UserAgentParser::Version.new('1.2.3') agent = UserAgentParser::UserAgent.new(nil, version) _(agent.version).must_equal version end end describe 'with os' do it 'sets os' do os = UserAgentParser::OperatingSystem.new('Windows XP') agent = UserAgentParser::UserAgent.new(nil, nil, os) _(agent.os).must_equal os end end describe 'with device' do it 'sets device' do device = UserAgentParser::Device.new('iPhone') agent = UserAgentParser::UserAgent.new(nil, nil, nil, device) _(agent.device).must_equal device end end end describe '#name' do it 'returns family' do agent = UserAgentParser::UserAgent.new('Safari') _(agent.name).must_equal agent.family end end describe '#==' do it 'returns true for same agents with no OS' do version = UserAgentParser::Version.new('1.0') agent1 = UserAgentParser::UserAgent.new('Chrome', version) agent2 = UserAgentParser::UserAgent.new('Chrome', version) _(agent1).must_equal agent2 end it 'returns true for same agents on same OS' do version = UserAgentParser::Version.new('1.0') os = UserAgentParser::OperatingSystem.new('Windows') agent1 = UserAgentParser::UserAgent.new('Chrome', version, os) agent2 = UserAgentParser::UserAgent.new('Chrome', version, os) _(agent1).must_equal agent2 end it 'returns false for same agent on different OS' do version = UserAgentParser::Version.new('1.0') windows = UserAgentParser::OperatingSystem.new('Windows') mac = UserAgentParser::OperatingSystem.new('Mac') agent1 = UserAgentParser::UserAgent.new('Chrome', version, windows) agent2 = UserAgentParser::UserAgent.new('Chrome', version, mac) _(agent1).wont_equal agent2 end it 'returns false for same os, but different browser version' do browser_version1 = UserAgentParser::Version.new('1.0') browser_version2 = UserAgentParser::Version.new('2.0') os = UserAgentParser::OperatingSystem.new('Windows') agent1 = UserAgentParser::UserAgent.new('Chrome', browser_version1, os) agent2 = UserAgentParser::UserAgent.new('Chrome', browser_version2, os) _(agent1).wont_equal agent2 end end describe '#eql?' do it 'returns true for same agents with no OS' do version = UserAgentParser::Version.new('1.0') agent1 = UserAgentParser::UserAgent.new('Chrome', version) agent2 = UserAgentParser::UserAgent.new('Chrome', version) assert_equal true, agent1.eql?(agent2) end it 'returns true for same agents on same OS' do version = UserAgentParser::Version.new('1.0') os = UserAgentParser::OperatingSystem.new('Windows') agent1 = UserAgentParser::UserAgent.new('Chrome', version, os) agent2 = UserAgentParser::UserAgent.new('Chrome', version, os) assert_equal true, agent1.eql?(agent2) end it 'returns false for same agent on different OS' do version = UserAgentParser::Version.new('1.0') windows = UserAgentParser::OperatingSystem.new('Windows') mac = UserAgentParser::OperatingSystem.new('Mac') agent1 = UserAgentParser::UserAgent.new('Chrome', version, windows) agent2 = UserAgentParser::UserAgent.new('Chrome', version, mac) assert_equal false, agent1.eql?(agent2) end it 'returns false for same os, but different browser version' do browser_version1 = UserAgentParser::Version.new('1.0') browser_version2 = UserAgentParser::Version.new('2.0') os = UserAgentParser::OperatingSystem.new('Windows') agent1 = UserAgentParser::UserAgent.new('Chrome', browser_version1, os) agent2 = UserAgentParser::UserAgent.new('Chrome', browser_version2, os) assert_equal false, agent1.eql?(agent2) end end describe '#inspect' do it 'returns the family and version' do browser_version = UserAgentParser::Version.new('1.0') agent = UserAgentParser::UserAgent.new('Chrome', browser_version) _(agent.inspect.to_s).must_equal '#' end it 'returns the OS if present' do browser_version = UserAgentParser::Version.new('1.0') os_version = UserAgentParser::Version.new('10.7.4') os = UserAgentParser::OperatingSystem.new('OS X', os_version) agent = UserAgentParser::UserAgent.new('Chrome', browser_version, os) _(agent.inspect).must_equal '#' end it 'returns device if present' do browser_version = UserAgentParser::Version.new('5.0.2') os_version = UserAgentParser::Version.new('4.2.1') os = UserAgentParser::OperatingSystem.new('iOS', os_version) device = UserAgentParser::Device.new('iPhone') agent = UserAgentParser::UserAgent.new('Mobile Safari', browser_version, os, device) _(agent.inspect).must_equal '#' end end describe '#to_h' do let(:expected) do { device: { family: 'iPhone', model: 'iPhone', brand: nil }, family: 'Mobile Safari', os: { version: { version: '4.2.1', major: '4', minor: '2', patch: '1', patch_minor: nil}, family: 'iOS' }, version: { version: '5.0.2', major: '5', minor: '0', patch: '2', patch_minor: nil } } end it 'returns everything' do browser_version = UserAgentParser::Version.new('5.0.2') os_version = UserAgentParser::Version.new('4.2.1') os = UserAgentParser::OperatingSystem.new('iOS', os_version) device = UserAgentParser::Device.new('iPhone') agent = UserAgentParser::UserAgent.new('Mobile Safari', browser_version, os, device) assert_equal(expected, agent.to_h) end end end ================================================ FILE: spec/version_spec.rb ================================================ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + '/spec_helper') describe UserAgentParser::Version do it "parses '1'" do version = UserAgentParser::Version.new('1') _(version.major).must_equal '1' end it "parses '1.2'" do version = UserAgentParser::Version.new('1.2') _(version.major).must_equal '1' _(version.minor).must_equal '2' end it "parses '1.2.3'" do version = UserAgentParser::Version.new('1.2.3') _(version.major).must_equal '1' _(version.minor).must_equal '2' _(version.patch).must_equal '3' end it "parses '1.2.3b4'" do version = UserAgentParser::Version.new('1.2.3b4') _(version.major).must_equal '1' _(version.minor).must_equal '2' _(version.patch).must_equal '3' _(version.patch_minor).must_equal 'b4' end it "parses '1.2.3-b4'" do version = UserAgentParser::Version.new('1.2.3-b4') _(version.major).must_equal '1' _(version.minor).must_equal '2' _(version.patch).must_equal '3' _(version.patch_minor).must_equal 'b4' end it "parses '1.2.3pre'" do version = UserAgentParser::Version.new('1.2.3pre') _(version.major).must_equal '1' _(version.minor).must_equal '2' _(version.patch).must_equal '3pre' end it "parses '1.2.3-45'" do version = UserAgentParser::Version.new('1.2.3-45') _(version.major).must_equal '1' _(version.minor).must_equal '2' _(version.patch).must_equal '3-45' end it 'accepts Fixnum and String arguments' do version = UserAgentParser::Version.new(1, '2a', 3, '4b') _(version.major).must_equal '1' _(version.minor).must_equal '2a' _(version.patch).must_equal '3' _(version.patch_minor).must_equal '4b' end describe '#to_s' do it 'returns the same string as initialized with' do version = UserAgentParser::Version.new('1.2.3b4') _(version.to_s).must_equal '1.2.3b4' end end describe '#==' do it 'returns true for same versions' do version = UserAgentParser::Version.new('1.2.3') _(version).must_equal UserAgentParser::Version.new('1.2.3') end it 'returns false for different versions' do version = UserAgentParser::Version.new('1.2.3') _(version).wont_equal UserAgentParser::Version.new('1.2.2') end end describe '#<=>' do it 'accepts string for comparison' do version = UserAgentParser::Version.new('1.2.3') assert_operator version, :<, '1.2.4' assert_operator version, :==, '1.2.3' assert_operator version, :>, '1.2.2' end it 'accepts another instance of Version for comparison' do version = UserAgentParser::Version.new('1.2.3') assert_operator version, :>, UserAgentParser::Version.new('1.2.2') assert_operator version, :==, UserAgentParser::Version.new('1.2.3') assert_operator version, :<, UserAgentParser::Version.new('1.2.4') end it 'is comparing major version' do version = UserAgentParser::Version.new('1.2.3') assert_operator version, :<, '2' assert_operator version, :>=, '1' assert_operator version, :>, '0' end it 'is comparing minor version' do version = UserAgentParser::Version.new('1.2.3') assert_operator version, :<, '2.0' assert_operator version, :<, '1.3' assert_operator version, :>=, '1.2' assert_operator version, :>, '1.1' assert_operator version, :>, '0.1' end it 'is comparing patch level' do version = UserAgentParser::Version.new('1.2.3') assert_operator version, :<, '1.2.4' assert_operator version, :>=, '1.2.3' assert_operator version, :<=, '1.2.3' assert_operator version, :>, '1.2.2' end it 'is comparing patch_minor level correctly' do version = UserAgentParser::Version.new('1.2.3.p1') assert_operator version, :<, '1.2.4' assert_operator version, :<, '1.2.3.p2' assert_operator version, :>=, '1.2.3.p1' assert_operator version, :<=, '1.2.3.p1' assert_operator version, :>, '1.2.3.p0' assert_operator version, :>, '1.2.2' assert_operator version, :>, '1.1' end it 'is correctly comparing versions with different lengths' do version = UserAgentParser::Version.new('1.42.3') assert_operator version, :<, '1.142' assert_operator version, :<, '1.42.4' assert_operator version, :>=, '1.42' assert_operator version, :>, '1.14' assert_operator version, :>, '1.7' assert_operator version, :>, '1.3' end it 'does its best to compare string versions' do version = UserAgentParser::Version.new('1.2.3.a') assert_operator version, :<, '1.2.4' assert_operator version, :<, '1.2.3.b' assert_operator version, :<, '1.2.3.p1' assert_operator version, :<, '1.2.3.p0' assert_operator version, :>, '1.2.2' end end describe '#inspect' do it 'returns the class and version' do version = UserAgentParser::Version.new('1.2.3') _(version.inspect).must_equal '#' end end end ================================================ FILE: user_agent_parser.gemspec ================================================ # frozen_string_literal: true Gem::Specification.new do |gem| gem.name = 'user_agent_parser' gem.version = '2.21.0' gem.authors = 'Tim Lucas' gem.email = 't@toolmantim.com' gem.homepage = 'https://github.com/ua-parser/uap-ruby' gem.summary = "Parsing user agent strings with the help of BrowserScope's UA database" gem.description = <<~DESCRIPTION A simple, comprehensive Ruby gem for parsing user agent strings with the help of BrowserScope's UserAgent database DESCRIPTION gem.license = 'MIT' gem.executables = ['user_agent_parser'] gem.files = %x{git ls-files}.split("\n").select { |d| d =~ %r{^(MIT-LICENSE|Readme.md|lib|bin/)} } + ['vendor/uap-core/regexes.yaml'] gem.required_ruby_version = '>= 2.5' end