device_map 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 74af4b49ff3b832ea9e7ec021cdad0725b082318
4
- data.tar.gz: 861c7beddeffa8e0cacab4cca54cf2bb8b42ac2a
3
+ metadata.gz: 85f3dc36e9ccf79015fe2c840d383d734e2a7758
4
+ data.tar.gz: 5a743460cb8cc085a1b75d83f158192a55ca784c
5
5
  SHA512:
6
- metadata.gz: 51c2bd18f0c6e73ec7d0c9ea5207afb1849ea87631db1b4610f76b62f86bf6e6723c4d821da3464a5e7869c18800bf55e781588c1225331aec7e5dbd265caaf0
7
- data.tar.gz: 60ed79f05b92e8f7c53df3fb45a2bd58b675703986d9f0348a4f1e86f8999b4a04fb7daf4efff19741fdb493dd2ecff17955397cb39ec4b9d237ed5cc749aba7
6
+ metadata.gz: 84ec35b4e0fae8acfd0da23b0394aaa97cc32282d65353094d1eb60ef6e01dc1b788ae83b9b4694ba6f7195ed9a38f6b17b05069d5996f5da4e4c6400fa2bd65
7
+ data.tar.gz: f909de648e1a309c6e9a8f8e7ee0036124173168728e66947313b85533db73ed33c1de7c8036fb8b2475380b02789428643a87dd915be513c3b5be42eec26247
data/.gitignore CHANGED
@@ -1,14 +1,4 @@
1
- /.bundle/
2
- /.yardoc
3
1
  /Gemfile.lock
4
- /_yardoc/
5
2
  /coverage/
6
3
  /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- *.bundle
11
- *.so
12
- *.o
13
- *.a
14
- mkmf.log
4
+ tags
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --warnings
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ Style/WordArray:
2
+ Enabled: false
3
+
4
+ Style/Documentation:
5
+ Enabled: false
6
+
7
+ Style/AlignParameters:
8
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.1
5
+ - 2.2
6
+ - rbx-2
7
+ before_install: bundle update --quiet
data/Gemfile CHANGED
@@ -1,4 +1,2 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in device_map.gemspec
4
2
  gemspec
data/LICENSE.txt CHANGED
@@ -1,22 +1,13 @@
1
- Copyright (c) 2014 Konstantin Papkovskiy
1
+ Copyright 2014 Konstantin Papkovskiy
2
2
 
3
- MIT License
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
4
6
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
7
+ http://www.apache.org/licenses/LICENSE-2.0
12
8
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # DeviceMap
2
+
3
+ Apache DeviceMap is a project to create a data repository containing device
4
+ information, images and other relevant information for all sorts of mobile
5
+ devices, e.g. smartphones and tablets.
6
+
7
+ [Apache DeviceMap](http://devicemap.apache.org/)
8
+
9
+ [![Build Status](https://travis-ci.org/soylent/device_map.svg?branch=master)](https://travis-ci.org/soylent/device_map)
10
+ [![Code Climate](https://codeclimate.com/github/soylent/device_map/badges/gpa.svg)](https://codeclimate.com/github/soylent/device_map)
11
+
12
+ ## Installation
13
+
14
+ Add `device_map` to your `Gemfile` and execute `bundle install`.
15
+
16
+ ## Basic example
17
+
18
+ ```ruby
19
+ require 'device_map'
20
+
21
+ user_agent = <<USERAGENT
22
+ Mozilla/5.0 (Linux; U; Android 2.3.4; en-gb; GT-I9100 Build/GINGERBREAD)
23
+ AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
24
+ USERAGENT
25
+
26
+ device = DeviceMap.classify(user_agent)
27
+
28
+ device.ajax_manipulate_css # => true
29
+ device.ajax_manipulate_dom # => true
30
+ device.ajax_support_event_listener # => true
31
+ device.ajax_support_events # => true
32
+ device.ajax_support_getelementbyid # => true
33
+ device.ajax_support_inner_html # => true
34
+ device.ajax_support_javascript # => true
35
+ device.device_os # => "Android"
36
+ device.device_os_version # => "2.3"
37
+ device.display_height # => 800
38
+ device.display_width # => 480
39
+ device.dual_orientation # => true
40
+ device.from # => "open_db_modified"
41
+ device.id # => "GT-I9100"
42
+ device.image_inlining # => true
43
+ device.input_devices # => "touchscreen"
44
+ device.marketing_name # => "Galaxy S II"
45
+ device.mobile_browser # => "Android Webkit"
46
+ device.mobile_browser_version # => "4.0"
47
+ device.model # => "GT-I9100"
48
+ ```
49
+
50
+ ## Contributing
51
+
52
+ Pull requests are very welcome. Please make sure that your changes
53
+ don't break the tests by running:
54
+
55
+ ```sh
56
+ $ bundle exec rake
57
+ ```
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rubocop/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ RuboCop::RakeTask.new
6
+
7
+ load 'ext/Rakefile'
8
+
9
+ desc 'Run tests and static code analyzer'
10
+ task ci: :prepare do
11
+ Rake::Task['spec'].invoke
12
+ Rake::Task['rubocop'].invoke
13
+ end
14
+
15
+ task default: :ci
data/device_map.gemspec CHANGED
@@ -1,23 +1,34 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'device_map/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "device_map"
6
+ spec.name = 'device_map'
8
7
  spec.version = DeviceMap::VERSION
9
- spec.authors = ["Konstantin Papkovskiy"]
10
- spec.email = ["konstantin@papkovskiy.com"]
11
- spec.summary = %q{Ruby client for Apache DeviceMap}
12
- spec.description = %q{Ruby client for Apache DeviceMap}
13
- spec.homepage = ""
14
- spec.license = "MIT"
8
+ spec.authors = ['Konstantin Papkovskiy']
9
+ spec.email = ['konstantin@papkovskiy.com']
10
+ spec.summary = 'Ruby client for Apache DeviceMap'
11
+ spec.description = <<-EOD
12
+ Ruby implementation of client for Apache DeviceMap repository
13
+ containing device information, images and other relevant
14
+ information for all sorts of mobile devices.
15
+ EOD
16
+
17
+ spec.homepage = 'https://github.com/soylent/device_map'
18
+ spec.license = 'Apache-2.0'
15
19
 
16
20
  spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
21
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(/^spec\//)
23
+ spec.require_paths = ['lib']
24
+ spec.extensions = ['ext/Rakefile']
25
+
26
+ spec.add_dependency 'nokogiri', '~> 1.6'
27
+
28
+ spec.add_development_dependency 'bundler', '~> 1.7'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rspec', '~> 3.1'
20
31
 
21
- spec.add_development_dependency "bundler", "~> 1.7"
22
- spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency 'pry', '~> 0.10'
33
+ spec.add_development_dependency 'rubocop', '~> 0.28'
23
34
  end
data/ext/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ # This task runs during installation of the gem
2
+ desc 'Prepare database dumps'
3
+ task :prepare do
4
+ lib = File.expand_path('../lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require 'device_map'
8
+
9
+ # TODO: Refactor
10
+
11
+ # Parse builder data and create inverted index
12
+ builder_xml = File.open(DeviceMap::BUILDER_DATA_SOURCE)
13
+ patterns = DeviceMap::DeviceData::Patterns.parse(builder_xml)
14
+
15
+ # Dump parsed builder data for later usage in <tt>DeviceMap::Classifier</tt>
16
+ patterns_dump = Marshal.dump(patterns)
17
+ File.write(DeviceMap::PATTERNS_DUMP, patterns_dump)
18
+
19
+ # Parse devices data
20
+ devices_xml = File.open(DeviceMap::DEVICE_DATA_SOURCE)
21
+ devices = DeviceMap::DeviceData::Devices.parse(devices_xml)
22
+
23
+ # Dump devices for later usage in <tt>DeviceMap::Classifier</tt>
24
+ devices_dump = Marshal.dump(devices)
25
+ File.write(DeviceMap::DEVICES_DUMP, devices_dump)
26
+ end
27
+
28
+ task default: :prepare
@@ -0,0 +1,38 @@
1
+ require 'singleton'
2
+
3
+ module DeviceMap
4
+ class Classifier
5
+ include Singleton
6
+
7
+ KEYWORD_NGRAM_SIZE = 4
8
+
9
+ attr_reader :patterns, :devices
10
+
11
+ def initialize
12
+ # TODO: Refactor
13
+ @patterns = Marshal.load(File.open(PATTERNS_DUMP))
14
+ @devices = Marshal.load(File.open(DEVICES_DUMP))
15
+ end
16
+
17
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
18
+ def find_device(ua)
19
+ user_agent = UserAgent.new(ua)
20
+ keyword_ngrams = user_agent.keyword_ngrams(KEYWORD_NGRAM_SIZE)
21
+
22
+ search_hits = keyword_ngrams.each_with_object(Set.new) do |ngram, hits|
23
+ hits.merge patterns.find(ngram.join)
24
+ end
25
+
26
+ matched_pattern = search_hits.sort.reverse.find do |pattern|
27
+ # FIXME: Match only against keyword hits
28
+ pattern.matches?(keyword_ngrams.map(&:join))
29
+ end
30
+
31
+ if matched_pattern
32
+ devices.find(matched_pattern.device_id)
33
+ else
34
+ DeviceData::Device.unknown
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,67 @@
1
+ module DeviceMap
2
+ module DeviceData
3
+ module Builder
4
+ class BuilderNotFound < StandardError; end
5
+
6
+ class << self
7
+ def find(builder_node_class)
8
+ builders.fetch(builder_node_class) do
9
+ fail BuilderNotFound,
10
+ "Could not find builder for #{builder_node_class}"
11
+ end
12
+ end
13
+
14
+ def register(klass, builder_class)
15
+ builders[builder_class] = klass
16
+ end
17
+
18
+ private
19
+
20
+ def builders
21
+ @builders ||= {}
22
+ end
23
+ end
24
+
25
+ class Simple < Struct.new(:priority)
26
+ def patterns(device_id, keywords)
27
+ keywords.map do |keyword|
28
+ Pattern.new(keyword, device_id, priority)
29
+ end
30
+ end
31
+ end
32
+
33
+ class TwoStep < Struct.new(:priority)
34
+ def patterns(device_id, keywords)
35
+ joined_keywords = Keyword.join(keywords)
36
+
37
+ [
38
+ Pattern.new(keywords, device_id, priority),
39
+ Pattern.new(joined_keywords, device_id, priority)
40
+ ]
41
+ end
42
+ end
43
+
44
+ # Creates OR patterns with normal priority
45
+ SIMPLE_BUILDER = Simple.new(1)
46
+
47
+ # Creates OR patterns with lower priority
48
+ GENERIC_BUILDER = Simple.new(0)
49
+
50
+ # Creates AND patterns with normal priority
51
+ TWO_STEP_BUILDER = TwoStep.new(1)
52
+
53
+ # rubocop:disable Metrics/LineLength
54
+
55
+ register SIMPLE_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.DesktopOSDeviceBuilder'
56
+ register GENERIC_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.SimpleDeviceBuilder'
57
+ register SIMPLE_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.BotDeviceBuilder'
58
+ register SIMPLE_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.AndroidDeviceBuilder'
59
+ register SIMPLE_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.SymbianDeviceBuilder'
60
+ register SIMPLE_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.WinPhoneDeviceBuilder'
61
+ register SIMPLE_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.IOSDeviceBuilder'
62
+ register TWO_STEP_BUILDER, 'org.apache.devicemap.simpleddr.builder.device.TwoStepDeviceBuilder'
63
+
64
+ # rubocop:enable Metrics/LineLength
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ module DeviceMap
2
+ module DeviceData
3
+ class Device
4
+ include Properties::DSL
5
+
6
+ property :ajax_manipulate_css, type: :boolean
7
+ property :ajax_manipulate_dom, type: :boolean
8
+ property :ajax_support_event_listener, type: :boolean
9
+ property :ajax_support_events, type: :boolean
10
+ property :ajax_support_getelementbyid, type: :boolean
11
+ property :ajax_support_inner_html, type: :boolean
12
+ property :ajax_support_javascript, type: :boolean
13
+ property :device_os, type: :string
14
+ property :device_os_version, type: :string
15
+ property :displayHeight, type: :integer, attr_name: :display_height
16
+ property :displayWidth, type: :integer, attr_name: :display_width
17
+ property :dual_orientation, type: :boolean
18
+ property :from, type: :string
19
+ property :id, type: :string
20
+ property :image_inlining, type: :boolean
21
+ property :inputDevices, type: :string, attr_name: :input_devices
22
+ property :is_bot, type: :boolean
23
+ property :is_desktop, type: :boolean
24
+ property :is_tablet, type: :boolean
25
+ property :is_wireless_device, type: :boolean
26
+ property :marketing_name, type: :string
27
+ property :mobile_browser, type: :string
28
+ property :mobile_browser_version, type: :string
29
+ property :model, type: :string
30
+ property :nokia_edition, type: :string
31
+ property :nokia_series, type: :string
32
+ property :vendor, type: :string
33
+ property :xhtml_format_as_attribute, type: :boolean
34
+ property :xhtml_format_as_css_property, type: :boolean
35
+
36
+ UNKNOWN_ID = 'unknown'
37
+
38
+ def self.parse(device_node)
39
+ properties = device_node.xpath('property')
40
+ attrs = properties.each_with_object({}) do |property, result|
41
+ result[property[:name]] = property[:value]
42
+ end
43
+
44
+ new(attrs.merge(id: device_node[:id]))
45
+ end
46
+
47
+ def self.unknown
48
+ new(id: UNKNOWN_ID)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ module DeviceMap
2
+ module DeviceData
3
+ class Devices
4
+ class DeviceNotFound < StandardError; end
5
+
6
+ def self.parse(devices_xml)
7
+ devices_doc = Nokogiri::XML(devices_xml)
8
+ devices = devices_doc.xpath('//device').map do |device_node|
9
+ Device.parse(device_node)
10
+ end
11
+
12
+ new(devices)
13
+ end
14
+
15
+ def initialize(devices)
16
+ @device_index = devices.each_with_object({}) do |device, device_index|
17
+ device_index[device.id] = device
18
+ end
19
+ end
20
+
21
+ def find(device_id)
22
+ # TODO: Return copy of <tt>Device</tt> object
23
+ @device_index.fetch(device_id) do
24
+ fail DeviceNotFound, "Cound not find device: #{device_id}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ require 'set'
2
+
3
+ module DeviceMap
4
+ module DeviceData
5
+ class Patterns
6
+ # rubocop:disable Metrics/MethodLength
7
+ def self.parse(openddr_builder_xml)
8
+ builders_doc = Nokogiri::XML(openddr_builder_xml)
9
+ openddr_builders = builders_doc.xpath('//builder')
10
+
11
+ all_patterns = openddr_builders.flat_map do |builder_node|
12
+ builder_node_class = builder_node[:class]
13
+ builder = Builder.find(builder_node_class)
14
+
15
+ builder_node.xpath('device').flat_map do |device_node|
16
+ device_id = device_node[:id]
17
+ keywords = device_node.xpath('list/value').map(&:content)
18
+ builder.patterns(device_id, keywords)
19
+ end
20
+ end
21
+
22
+ new(all_patterns)
23
+ end
24
+
25
+ def initialize(all_patterns)
26
+ @pattern_index = {}
27
+
28
+ all_patterns.each do |pattern|
29
+ pattern.keywords.each do |keyword|
30
+ @pattern_index[keyword] ||= Set.new
31
+ @pattern_index[keyword] << pattern
32
+ end
33
+ end
34
+ end
35
+
36
+ def find(keyword)
37
+ # TODO: Return copy of the set
38
+ @pattern_index.fetch(keyword) do
39
+ Set.new
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ module DeviceMap
2
+ module Keyword
3
+ # Deletes all non-alphanumeric characters from the given keywords
4
+ def self.normalize(keywords)
5
+ keywords.map do |keyword|
6
+ # HACK: <tt>BUILDER_DATA_SOURCE</tt> contains keywords like:
7
+ # <device id="BlackBerry 9650">
8
+ # <list>
9
+ # <value>[Bb]lack.?[Bb]erry</value>
10
+ # <value>blackberry 9650</value>
11
+ # </list>
12
+ # </device>
13
+ # In such cases we want to replace patterns with simple keywords.
14
+ keyword.downcase.gsub('[bb]', 'b').gsub(/[\W_]+/, '')
15
+ end
16
+ end
17
+
18
+ # Concatenate all keywords together and skip duplicates
19
+ def self.join(keywords)
20
+ # HACK: This function handles the case when we want to concatenate all
21
+ # keywords without duplication.
22
+ # <device id="BlackBerry 9700">
23
+ # <list>
24
+ # <value>blackberry</value>
25
+ # <value>blackberry 9700</value>
26
+ # </list>
27
+ # </device>
28
+ normalize(keywords).reduce('') do |result, keyword|
29
+ if keyword.include?(result)
30
+ keyword
31
+ else
32
+ result.concat(keyword)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ module DeviceMap
2
+ class Pattern
3
+ include Comparable
4
+
5
+ attr_reader :keywords, :device_id, :priority
6
+
7
+ def initialize(keywords, device_id, priority)
8
+ @keywords = Keyword.normalize(Array(keywords))
9
+ @device_id = device_id
10
+ @priority = priority
11
+ end
12
+
13
+ def matches?(other_keywords)
14
+ diff = keywords - other_keywords
15
+ diff.empty?
16
+ end
17
+
18
+ def <=>(other)
19
+ if priority == other.priority
20
+ keywords.join.size <=> other.keywords.join.size
21
+ else
22
+ priority <=> other.priority
23
+ end
24
+ end
25
+
26
+ def ==(other)
27
+ keywords == other.keywords &&
28
+ device_id == other.device_id &&
29
+ priority == other.priority
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,48 @@
1
+ module DeviceMap
2
+ module Properties
3
+ class UnknownProperty < StandardError; end
4
+
5
+ module DSL
6
+ module ClassMethods
7
+ def property(name, type: :string, attr_name: name)
8
+ attr_reader attr_name
9
+ properties[name] = Property.new(name, type, attr_name)
10
+ end
11
+
12
+ # FIXME: This method should not be public
13
+ def properties
14
+ @properties ||= {}
15
+ end
16
+ end
17
+
18
+ def self.included(base)
19
+ base.extend(ClassMethods)
20
+ end
21
+
22
+ def initialize(attrs)
23
+ attrs.each do |name, value|
24
+ property = properties.fetch(name.to_sym) do
25
+ fail UnknownProperty, "Property #{name} is not defined"
26
+ end
27
+
28
+ attr_name = property.attr_name
29
+ casted_value = property.cast(value)
30
+ instance_variable_set(:"@#{attr_name}", casted_value)
31
+ end
32
+ end
33
+
34
+ def ==(other)
35
+ properties.all? do |_, property|
36
+ attr_name = property.attr_name
37
+ public_send(attr_name) == other.public_send(attr_name)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def properties
44
+ self.class.properties
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ module DeviceMap
2
+ module Properties
3
+ class Property < Struct.new(:name, :type_name, :attr_name)
4
+ TYPE_MAPPING = {
5
+ integer: Types::Integer,
6
+ boolean: Types::Boolean,
7
+ string: Types::String
8
+ }
9
+
10
+ def cast(value)
11
+ return if value.nil?
12
+
13
+ type = TYPE_MAPPING.fetch(type_name)
14
+ type.cast(value)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ module DeviceMap
2
+ module Properties
3
+ module Types
4
+ module Integer
5
+ def self.cast(value)
6
+ value.to_i
7
+ end
8
+ end
9
+
10
+ module Boolean
11
+ def self.cast(value)
12
+ case value
13
+ when 'true' then true
14
+ when 'false' then false
15
+ else fail ArgumentError, "Cannot cast #{value} to boolean"
16
+ end
17
+ end
18
+ end
19
+
20
+ module String
21
+ def self.cast(value)
22
+ value.to_s
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module DeviceMap
2
+ class UserAgent
3
+ def initialize(user_agent)
4
+ @user_agent = user_agent
5
+ end
6
+
7
+ def keyword_ngrams(size)
8
+ keywords = @user_agent.split(/[\s;\-_\/()\[\]\\]+/)
9
+ normalized_keywords = Keyword.normalize(keywords)
10
+
11
+ normalized_keywords.flat_map.with_index do |keyword, i|
12
+ size.times.map do |j|
13
+ next_keywords = normalized_keywords[i + 1..-1] || []
14
+ Array(keyword).concat next_keywords.take(j)
15
+ end
16
+ end.uniq
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,4 @@
1
1
  module DeviceMap
2
- VERSION = "0.0.1"
2
+ VERSION = '0.1.0'
3
+ DATA_VERSION = '1.0.1'
3
4
  end