File: parser.rb

package info (click to toggle)
ruby-user-agent-parser 2.5.1-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 256 kB
  • sloc: ruby: 374; makefile: 6
file content (167 lines) | stat: -rw-r--r-- 4,494 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# frozen_string_literal: true

require 'yaml'

module UserAgentParser
  class Parser
    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_path

    def initialize(options = {})
      @patterns_path = options[:patterns_path] || UserAgentParser::DefaultPatternsPath
      @ua_patterns, @os_patterns, @device_patterns = load_patterns(patterns_path)
    end

    def parse(user_agent)
      os = parse_os(user_agent)
      device = parse_device(user_agent)
      parse_ua(user_agent, os, device)
    end

    private

    def load_patterns(path)
      yml = YAML.load_file(path)

      # Parse all the regexs
      yml.each_pair do |_type, patterns|
        patterns.each do |pattern|
          pattern[:regex] = Regexp.new(pattern['regex'], pattern['regex_flag'] == 'i')
        end
      end

      [yml['user_agent_parsers'], yml['os_parsers'], yml['device_parsers']]
    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 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

    if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4.0')
      def first_pattern_match(patterns, value)
        patterns.each do |pattern|
          if pattern[:regex].match?(value)
            return [pattern, pattern[:regex].match(value)]
          end
        end
        nil
      end
    else
      def first_pattern_match(patterns, value)
        patterns.each do |pattern|
          if (match = pattern[:regex].match(value))
            return [pattern, match]
          end
        end
        nil
      end
    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