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 +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
|
+
[](https://travis-ci.org/soylent/device_map)
|
10
|
+
[](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