device_map 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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