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 +4 -4
- data/.gitignore +1 -11
- data/.rspec +2 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +7 -0
- data/Gemfile +0 -2
- data/LICENSE.txt +10 -19
- data/README.md +57 -0
- data/Rakefile +15 -0
- data/device_map.gemspec +25 -14
- data/ext/Rakefile +28 -0
- data/lib/device_map/classifier.rb +38 -0
- data/lib/device_map/device_data/builder.rb +67 -0
- data/lib/device_map/device_data/device.rb +52 -0
- data/lib/device_map/device_data/devices.rb +29 -0
- data/lib/device_map/device_data/patterns.rb +44 -0
- data/lib/device_map/keyword.rb +37 -0
- data/lib/device_map/pattern.rb +32 -0
- data/lib/device_map/properties/dsl.rb +48 -0
- data/lib/device_map/properties/property.rb +18 -0
- data/lib/device_map/properties/types.rb +27 -0
- data/lib/device_map/user_agent.rb +19 -0
- data/lib/device_map/version.rb +2 -1
- data/lib/device_map.rb +32 -2
- data/lib/resources/BuilderDataSource.xml +14992 -0
- data/lib/resources/DeviceDataSource.xml +44706 -0
- data/spec/classifier_spec.rb +44 -0
- data/spec/device_data/builder_spec.rb +65 -0
- data/spec/device_data/device_spec.rb +26 -0
- data/spec/device_data/devices_spec.rb +37 -0
- data/spec/device_data/patterns_spec.rb +52 -0
- data/spec/device_map_spec.rb +10 -0
- data/spec/keyword_spec.rb +32 -0
- data/spec/pattern_spec.rb +49 -0
- data/spec/properties/dsl_spec.rb +61 -0
- data/spec/user_agent_spec.rb +46 -0
- metadata +107 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85f3dc36e9ccf79015fe2c840d383d734e2a7758
|
4
|
+
data.tar.gz: 5a743460cb8cc085a1b75d83f158192a55ca784c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84ec35b4e0fae8acfd0da23b0394aaa97cc32282d65353094d1eb60ef6e01dc1b788ae83b9b4694ba6f7195ed9a38f6b17b05069d5996f5da4e4c6400fa2bd65
|
7
|
+
data.tar.gz: f909de648e1a309c6e9a8f8e7ee0036124173168728e66947313b85533db73ed33c1de7c8036fb8b2475380b02789428643a87dd915be513c3b5be42eec26247
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,22 +1,13 @@
|
|
1
|
-
Copyright
|
1
|
+
Copyright 2014 Konstantin Papkovskiy
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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 =
|
6
|
+
spec.name = 'device_map'
|
8
7
|
spec.version = DeviceMap::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
|
14
|
-
|
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(
|
18
|
-
spec.test_files = spec.files.grep(
|
19
|
-
spec.require_paths = [
|
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
|
22
|
-
spec.add_development_dependency
|
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
|
data/lib/device_map/version.rb
CHANGED