trifle 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+
3
+ # Specify your gem's dependencies in messages.gemspec
4
+ gemspec
5
+
6
+ gem "fakeredis", github: "caius/fakeredis", branch: "redis-rb-3"
data/Gemfile.lock ADDED
@@ -0,0 +1,36 @@
1
+ GIT
2
+ remote: git://github.com/caius/fakeredis.git
3
+ revision: ab336ff1edf50c259b557559f42316fe15a4feb7
4
+ branch: redis-rb-3
5
+ specs:
6
+ fakeredis (0.3.1)
7
+ redis (>= 3.0.0)
8
+
9
+ PATH
10
+ remote: .
11
+ specs:
12
+ trifle (0.0.1)
13
+
14
+ GEM
15
+ remote: http://rubygems.org/
16
+ specs:
17
+ diff-lcs (1.1.3)
18
+ redis (3.0.1)
19
+ rspec (2.10.0)
20
+ rspec-core (~> 2.10.0)
21
+ rspec-expectations (~> 2.10.0)
22
+ rspec-mocks (~> 2.10.0)
23
+ rspec-core (2.10.1)
24
+ rspec-expectations (2.10.0)
25
+ diff-lcs (~> 1.1.3)
26
+ rspec-mocks (2.10.1)
27
+ rujitsu (0.3.3)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ fakeredis!
34
+ rspec
35
+ rujitsu
36
+ trifle!
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Trifle
2
+
3
+ Stores the GeoIP databases in Redis and gives it a simple way to lookup IPs and map them to countries/country codes.
4
+
5
+ Get your GeoIP country CSVs from http://www.maxmind.com/app/geolite.
6
+
7
+ Trifle supports both the IPV4 and IPV6 databases.
8
+
9
+ ## Installation
10
+
11
+ Install the gem
12
+
13
+ ```
14
+ gem install trifle
15
+ ```
16
+
17
+ or add it to your Gemfile and bundle
18
+
19
+ ```ruby
20
+ gem "trifle"
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Loading
26
+
27
+ ```ruby
28
+ # Initialize
29
+ trifle = Trifle.new(Redis.new)
30
+ # Load data from file
31
+ trifle.load filename: "file.csv"
32
+ # or files
33
+ trifle.load filenames: ["file1.csv", "file2.csv"]
34
+ # or directly as an array
35
+ trifle.load data: preloaded_array
36
+ ```
37
+
38
+ ### Lookup
39
+
40
+ ```ruby
41
+ # Initialize
42
+ trifle = Trifle.new(Redis.new)
43
+
44
+ # Lookup for existing data
45
+ trifle.find "223.255.128.0"
46
+ # => ["HK", "Hong Kong"]
47
+
48
+ # Lookup for missing data
49
+ trifle.find "192.168.1.1"
50
+ # => nil
51
+ ```
52
+
53
+
54
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/trifle.rb ADDED
@@ -0,0 +1,29 @@
1
+ # Trifle is a simple storage and lookup gem for GeoIP data
2
+ # using Redis as storage
3
+ require "trifle/loader"
4
+ require "trifle/finder"
5
+ require "trifle/initialize_with_redis"
6
+
7
+ class Trifle
8
+ include InitializeWithRedis
9
+
10
+ KEY = "trifle".freeze
11
+
12
+ def load options = {}
13
+ loader.handle options
14
+ end
15
+
16
+ def find ip
17
+ finder.handle ip
18
+ end
19
+
20
+ protected
21
+
22
+ def loader
23
+ @loader = Loader.new(redis)
24
+ end
25
+
26
+ def finder
27
+ @finder = Finder.new(redis)
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ require_relative "initialize_with_redis"
2
+ require 'ipaddr'
3
+
4
+ class Trifle
5
+ class Finder
6
+ include InitializeWithRedis
7
+
8
+ def handle ip
9
+ ip_i = IPAddr.new(ip).to_i
10
+ find(ip_i, 0, max)
11
+ end
12
+
13
+ protected
14
+
15
+ def find ip_i, lower, upper
16
+ return nil if lower > upper
17
+
18
+ index = (lower+upper)/2
19
+
20
+ current = entry_for(index)
21
+
22
+ if in_range(current, ip_i)
23
+ current.last(2)
24
+ elsif ip_i < current[0]
25
+ find ip_i, lower, index-1
26
+ else
27
+ find ip_i, index+1, upper
28
+ end
29
+ end
30
+
31
+ def max
32
+ redis.llen Trifle::KEY
33
+ end
34
+
35
+ def entry_for index
36
+ entry = redis.lindex(Trifle::KEY, index).split(":")
37
+ entry[0] = entry[0].to_i
38
+ entry[1] = entry[1].to_i
39
+ entry
40
+ end
41
+
42
+ def in_range range, ip_i
43
+ ip_i >= range[0] && ip_i <= range[1]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ class Trifle
2
+ module InitializeWithRedis
3
+ attr_accessor :redis
4
+
5
+ def initialize redis
6
+ raise ArgumentError.new("Redis object expected") unless redis.is_a?(Redis)
7
+ self.redis = redis
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,69 @@
1
+ require_relative "initialize_with_redis"
2
+ require "csv"
3
+
4
+ class Trifle
5
+ class Loader
6
+ include InitializeWithRedis
7
+
8
+ def handle options = {}
9
+ if options[:filename]
10
+ load_files [options[:filename]]
11
+ elsif options[:filenames]
12
+ load_files options[:filenames]
13
+ elsif options[:data]
14
+ load_data options[:data]
15
+ else
16
+ raise ArgumentError.new("Please provide a :filename, :filenames or an array of :data as an argument.")
17
+ end
18
+ end
19
+
20
+ def load_files filenames
21
+ raise ArgumentError.new("filenames must be an array of strings") unless filenames.is_a?(Array) && !filenames.map{|element| element.is_a?(String)}.include?(false)
22
+
23
+ data = []
24
+ filenames.each do |filename|
25
+ contents = File.open(filename, "rb").read
26
+ data += parse(contents)
27
+ end
28
+ load_data data
29
+ end
30
+
31
+ def load_data data
32
+ raise ArgumentError.new("data must be an array as loaded from a GeoIP data set") unless valid?(data)
33
+ clear
34
+ sort(data)
35
+ data.each {|row| append(row) }
36
+ end
37
+
38
+ def append row
39
+ entry = row.values_at(2,3,4,5).join(":")
40
+ redis.rpush Trifle::KEY, entry
41
+ end
42
+
43
+ def sort data
44
+ data.sort {|a,b| a[2] <=> b[2] }
45
+ end
46
+
47
+ def parse contents
48
+ contents.gsub!('", "', '","')
49
+ CSV.parse(contents)
50
+ end
51
+
52
+ def valid? data
53
+ if data.is_a?(Array) && data.count > 0
54
+ return data.map do |row|
55
+ is_number(row[2]) && is_number(row[3])
56
+ end.select{|valid| !valid}.count == 0
57
+ end
58
+ false
59
+ end
60
+
61
+ def is_number field
62
+ field.is_a?(Numeric) || field =~ /^\d+$/
63
+ end
64
+
65
+ def clear
66
+ redis.del(Trifle::KEY)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ class Trifle
2
+ VERSION = "0.0.1" unless defined? Trifle::VERSION
3
+ end
@@ -0,0 +1,3 @@
1
+ require "trifle"
2
+ require "rujitsu"
3
+ require "fakeredis/rspec"
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe Trifle::Finder do
4
+
5
+ before do
6
+ @valid_data = [
7
+ ["223.255.128.0","223.255.191.255","3758063616","3758079999","HK","Hong Kong"],
8
+ ["223.255.192.0","223.255.223.255","3758080000","3758088191","KR","Korea, Republic of"],
9
+ ["223.255.224.0","223.255.231.255","3758088192","3758090239","ID","Indonesia"],
10
+ ["223.255.232.0","223.255.235.255","3758090240","3758091263","AU","Australia"],
11
+ ["223.255.236.0","223.255.239.255","3758091264","3758092287","CN","China"],
12
+ ["223.255.240.0","223.255.243.255","3758092288","3758093311","HK","Hong Kong"],
13
+ ["223.255.244.0","223.255.247.255","3758093312","3758094335","IN","India"],
14
+ ["223.255.248.0","223.255.251.255","3758094336","3758095359","AU","Australia"],
15
+ ["223.255.252.0","223.255.253.255","3758095360","3758095871","CN","China"],
16
+ ["223.255.254.0","223.255.254.255","3758095872","3758096127","SG","Singapore"],
17
+ ["223.255.255.0","223.255.255.255","3758096128","3758096383","AU","Australia"]
18
+ ]
19
+ @redis = Redis.new
20
+ trifle = Trifle.new(@redis)
21
+ @finder = Trifle::Finder.new(@redis)
22
+ trifle.load(data: @valid_data)
23
+ end
24
+
25
+ describe "#handle" do
26
+ it "should find the right entry" do
27
+ @finder.handle("223.255.128.0").should be == ["HK", "Hong Kong"]
28
+ @finder.handle("223.255.244.10").should be == ["IN", "India"]
29
+ @finder.handle("223.255.255.255").should be == ["AU","Australia"]
30
+ end
31
+
32
+ it "should return nil if the entry wasn't found" do
33
+ @finder.handle("127.0.0.1").should be_nil
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe Trifle::InitializeWithRedis do
4
+
5
+ describe "#initialize" do
6
+ before do
7
+ @klass = Class.new do
8
+ include Trifle::InitializeWithRedis
9
+ end
10
+ end
11
+
12
+ it "should accept a redis instance" do
13
+ @klass.new(Redis.new).should be_a(@klass)
14
+ end
15
+
16
+ it "should fail without a redis instance" do
17
+ -> { @klass.new(nil) }.should raise_error(ArgumentError)
18
+ -> { @klass.new("http://127.0.0.1:3000/") }.should raise_error(ArgumentError)
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,169 @@
1
+ require "spec_helper"
2
+
3
+ describe Trifle::Loader do
4
+
5
+ before do
6
+ @loader = Trifle::Loader.new(Redis.new)
7
+ @valid_data = [
8
+ ["223.255.254.0","223.255.254.255","3758095872","3758096127","SG","Singapore"],
9
+ ["223.255.255.0","223.255.255.255","3758096128","3758096383","AU","Australia"]
10
+ ]
11
+ @csv = <<-EOD
12
+ "223.255.254.0","223.255.254.255","3758095872","3758096127","SG","Singapore"
13
+ "223.255.255.0","223.255.255.255","3758096128","3758096383","AU","Australia"
14
+ EOD
15
+ end
16
+
17
+ it "should extend InitializeWithRedis" do
18
+ Trifle::Loader.included_modules.should include(Trifle::InitializeWithRedis)
19
+ end
20
+
21
+ describe "#handle" do
22
+
23
+ context "when given the :filename option" do
24
+ it "should pass it on to load_files" do
25
+ filename = 'data.csv'
26
+ @loader.should_receive(:load_files).with([filename])
27
+ @loader.handle(filename: filename)
28
+ end
29
+ end
30
+
31
+ context "when given the :filenames option" do
32
+ it "should pass it on to load_files" do
33
+ filenames = ['data1.csv', 'data2.csv']
34
+ @loader.should_receive(:load_files).with(filenames)
35
+ @loader.handle(filenames: filenames)
36
+ end
37
+ end
38
+
39
+ context "when given the :data option" do
40
+ it "should pass it on to load_data" do
41
+ data = [:foo, :bar]
42
+ @loader.should_receive(:load_data).with(data)
43
+ @loader.handle(data: data)
44
+ end
45
+ end
46
+
47
+ context "when given anything else" do
48
+ it "should raise an error" do
49
+ -> { @loader.handle(foo: :bar) }.should raise_error(ArgumentError)
50
+ -> { @loader.handle }.should raise_error(ArgumentError)
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "#load_files" do
56
+ context "when given a valid list of files" do
57
+ before do
58
+ @filenames = ['foo.csv', 'bar.csv']
59
+ file = mock File
60
+ file.should_receive(:read).twice.and_return(@csv)
61
+ File.should_receive(:open).twice.and_return(file)
62
+ end
63
+
64
+ it "should read the files" do
65
+ @loader.load_files @filenames
66
+ @loader.redis.llen(Trifle::KEY).should be == 4
67
+ end
68
+ end
69
+
70
+ context "when given anything but an array of strings" do
71
+ it "should raise an error" do
72
+ -> { @loader.load_files "" }.should raise_error(ArgumentError)
73
+ -> { @loader.load_files [:foo] }.should raise_error(ArgumentError)
74
+ end
75
+ end
76
+
77
+ context "when given a filename for a file that's missing" do
78
+ it "should raise an error" do
79
+ -> { @loader.load_files ["foobar.csv"] }.should raise_error(/No such file or directory - foobar.csv/)
80
+ end
81
+ end
82
+ end
83
+
84
+ describe "#load_data" do
85
+
86
+ context "when given valid data" do
87
+ it "should clear out existing data" do
88
+ @loader.should_receive(:clear)
89
+ @loader.load_data @valid_data
90
+ end
91
+
92
+ it "should validate the data" do
93
+ @loader.should_receive(:valid?).and_return(true)
94
+ @loader.load_data @valid_data
95
+ end
96
+
97
+ it "should load it in redis" do
98
+ @loader.load_data @valid_data
99
+ @loader.redis.llen(Trifle::KEY).should be == 2
100
+ end
101
+ end
102
+
103
+ context "when given invalid data" do
104
+ it "should raise an error" do
105
+ @loader.should_receive(:valid?).and_return(false)
106
+ -> { @loader.load_data @valid_data }.should raise_error(ArgumentError)
107
+ end
108
+ end
109
+
110
+ context "when given anything but an array" do
111
+ it "should raise an error" do
112
+ -> { @loader.load_data :rubbish }.should raise_error(ArgumentError)
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "#parse" do
118
+ it "should parse valid csv data" do
119
+ @loader.parse(@csv).should be == @valid_data
120
+ end
121
+
122
+ it "should raise an error for invalid csv data" do
123
+ -> { @loader.parse('"foo",","#') }.should raise_error(CSV::MalformedCSVError)
124
+ end
125
+
126
+ it "should handle ipv6 data" do
127
+ @ipv6csv = <<-CSV
128
+ "2c0f:ffe8::", "2c0f:ffe8:ffff:ffff:ffff:ffff:ffff:ffff", "58569106662796955307479896348547874816", "58569106742025117821744233942091825151", "NG", "Nigeria"
129
+ "2c0f:fff0::", "2c0f:fff0:ffff:ffff:ffff:ffff:ffff:ffff", "58569107296622255421594597096899477504", "58569107375850417935858934690443427839", "NG", "Nigeria"
130
+ CSV
131
+ -> { @loader.parse(@ipv6csv) }.should_not raise_error
132
+ end
133
+ end
134
+
135
+ describe "#sort" do
136
+ it "should sort the data" do
137
+ @loader.sort(@valid_data.reverse).should be == @valid_data
138
+ end
139
+ end
140
+
141
+
142
+ describe "#valid?" do
143
+ it "should mark valid data as such" do
144
+ @loader.valid?(@valid_data).should be_true
145
+ end
146
+
147
+ it "should mark invalid data as such" do
148
+ invalid_data = []
149
+ @loader.valid?(invalid_data).should be_false
150
+ invalid_data = [
151
+ ["223.255.254.0","223.255.254.255","not_a_number","3758096127","SG","Singapore"]
152
+ ]
153
+ @loader.valid?(invalid_data).should be_false
154
+ invalid_data = [
155
+ ["223.255.254.0","223.255.254.255","3758095872","not_a_number","SG","Singapore"]
156
+ ]
157
+ @loader.valid?(invalid_data).should be_false
158
+ end
159
+ end
160
+
161
+ describe "#clear" do
162
+ it "should clear existing data" do
163
+ @loader.load_data @valid_data
164
+ @loader.clear
165
+ @loader.redis.llen(Trifle::KEY).should be == 0
166
+ end
167
+ end
168
+
169
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe Trifle do
4
+
5
+ it "should extend InitializeWithRedis" do
6
+ Trifle.included_modules.should include(Trifle::InitializeWithRedis)
7
+ end
8
+
9
+ describe "#load" do
10
+ before do
11
+ @trifle = Trifle.new(Redis.new)
12
+ @options = { filenames: "foobar.csv" }
13
+ end
14
+
15
+ it "should pass this to the loader" do
16
+ loader = mock Trifle::Loader
17
+ Trifle::Loader.should_receive(:new).and_return(loader)
18
+ loader.should_receive(:handle).with(@options)
19
+ @trifle.load @options
20
+ end
21
+ end
22
+
23
+ describe "#find" do
24
+ before do
25
+ @trifle = Trifle.new(Redis.new)
26
+ @ip = "127.0.0.1"
27
+ end
28
+
29
+ it "should pass this to the finder" do
30
+ finder = mock Trifle::Finder
31
+ Trifle::Finder.should_receive(:new).and_return(finder)
32
+ finder.should_receive(:handle).with(@ip)
33
+ @trifle.find @ip
34
+ end
35
+ end
36
+
37
+ end
data/trifle.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "trifle/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "trifle"
7
+ s.version = Trifle::VERSION
8
+ s.authors = ["Cristiano Betta"]
9
+ s.email = ["cristiano@emberads.com"]
10
+ s.homepage = "http://emberads.com"
11
+ s.summary = "A GeoIP lookup in Redis"
12
+ s.description = "Stores the GeoIP databases in Redis and gives it a simple way to lookup IPs and map them to countries"
13
+
14
+ s.rubyforge_project = "trifle"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency 'rspec'
22
+ s.add_development_dependency 'rujitsu'
23
+ s.add_development_dependency "fakeredis", "~> 0.3.0"
24
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: trifle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Cristiano Betta
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70353203055680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70353203055680
25
+ - !ruby/object:Gem::Dependency
26
+ name: rujitsu
27
+ requirement: &70353203079000 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70353203079000
36
+ - !ruby/object:Gem::Dependency
37
+ name: fakeredis
38
+ requirement: &70353203078460 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 0.3.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70353203078460
47
+ description: Stores the GeoIP databases in Redis and gives it a simple way to lookup
48
+ IPs and map them to countries
49
+ email:
50
+ - cristiano@emberads.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - .rspec
57
+ - Gemfile
58
+ - Gemfile.lock
59
+ - README.md
60
+ - Rakefile
61
+ - lib/trifle.rb
62
+ - lib/trifle/finder.rb
63
+ - lib/trifle/initialize_with_redis.rb
64
+ - lib/trifle/loader.rb
65
+ - lib/trifle/version.rb
66
+ - spec/spec_helper.rb
67
+ - spec/trifle/finder_spec.rb
68
+ - spec/trifle/initialize_with_redis_spec.rb
69
+ - spec/trifle/loader_spec.rb
70
+ - spec/trifle_spec.rb
71
+ - trifle.gemspec
72
+ homepage: http://emberads.com
73
+ licenses: []
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project: trifle
92
+ rubygems_version: 1.8.10
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: A GeoIP lookup in Redis
96
+ test_files:
97
+ - spec/spec_helper.rb
98
+ - spec/trifle/finder_spec.rb
99
+ - spec/trifle/initialize_with_redis_spec.rb
100
+ - spec/trifle/loader_spec.rb
101
+ - spec/trifle_spec.rb