shrinker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --debugger
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3-p194@shrinker --install --create
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shrinker.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
7
+ gem 'debugger'
8
+ gem 'redis'
9
+ gem 'mail'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 jrichardlai
2
+
3
+ MIT License
4
+
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:
12
+
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.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Shrinker
2
+
3
+ Replace all occurence of a domain and create a shortened link using a token.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'shrinker'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install shrinker
18
+
19
+ ## Usage
20
+
21
+
22
+ ```ruby
23
+ Shrinker.configure do
24
+ backend 'Redis'
25
+ backend_options client: {port: 6388, host: '192.168.12.22'}
26
+ expanded_domain /(www.)?google.com/ # this also works with protocol
27
+ exclude_path /assets|images/
28
+ anchors\_only\_in_html true # With the mime only replace the links inside an anchor
29
+ shrinked_domain 'go.com'
30
+ end
31
+ ```
32
+
33
+ Not shrinking a link `www.google.com?shrinker=false&something=else` would be replaced by `www.google.com?something=else`.
34
+
35
+ Usage on a text:
36
+
37
+ ```ruby
38
+
39
+ text = <<-EV
40
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
41
+ Nunc quis rutrum http://www.google.com?something=true&else=false dolor.
42
+ Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
43
+ Curabitur ullamcorper nisl non dolor http://google.fr?something=true venenatis consequat.
44
+ Morbi odio libero, tincidunt quis tempus a, fringilla vitae augue.
45
+ http://google.com/somepath?something=true
46
+ Aenean placerat ullamcorper lorem vel feugiat.
47
+ EV
48
+
49
+
50
+ new_text = Shrinker::Parser::Text.replace(text, {:user_id => 123})
51
+
52
+ new_text # =>
53
+ # Lorem ipsum dolor sit amet, consectetur adipiscing elit.
54
+ # Nunc quis rutrum http://go.com/token1 dolor.
55
+ # Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
56
+ # Curabitur ullamcorper nisl non dolor http://google.fr?something=true venenatis consequat.
57
+ # Morbi odio libero, tincidunt quis tempus a, fringilla vitae augue.
58
+ # http://go.com/token2
59
+ # Aenean placerat ullamcorper lorem vel feugiat.
60
+
61
+ ```
62
+
63
+ With a MIME:
64
+
65
+ ```ruby
66
+ new_mime = Shrinker::Parser::Mime.replace(mime, {:user_id => 123})
67
+ ```
68
+
69
+ Get back a real link:
70
+
71
+ ```ruby
72
+ url = Shrinker::unshrink(token)
73
+ url.to_s # => 'http://google.com/something=true'
74
+ url.attributes['user_id'] # => 123
75
+ ```
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,25 @@
1
+ module Shrinker
2
+ module Backend
3
+ class Abstract
4
+ attr_reader :options
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ end
9
+
10
+ def store(raw_url, token, attributes = {})
11
+ raise "Shrinker::Backend::Abstract.store not implemented"
12
+ end
13
+
14
+ def fetch(token)
15
+ raise "Shrinker::Backend::Abstract.fetch not implemented"
16
+ end
17
+
18
+ private
19
+
20
+ def ttl
21
+ options[:expires_in]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ module Shrinker
2
+ module Backend
3
+ class Redis < Abstract
4
+ require 'redis'
5
+
6
+ def initialize(options = {})
7
+ super
8
+ @client_options = options[:client] || {}
9
+ @namespace = options[:namespace] || '_shrinker'
10
+ end
11
+
12
+ def store(raw_url, token, attributes = {})
13
+ content = Shrinker::Serializer::to_json(raw_url, attributes)
14
+ redis_key = key(token)
15
+ if ttl
16
+ client.setex(redis_key, ttl, content)
17
+ else
18
+ client.set(redis_key, content)
19
+ end
20
+ end
21
+
22
+ def fetch(token)
23
+ content = client.get(key(token))
24
+
25
+ return {} unless content
26
+ Shrinker::Serializer::from_json(content)
27
+ end
28
+
29
+ def used_token?(token)
30
+ !!client.get(key(token))
31
+ end
32
+
33
+ protected
34
+
35
+ def client
36
+ @client ||= begin
37
+ ::Redis.new(@client_options)
38
+ end
39
+ end
40
+
41
+ def delete(token)
42
+ client.del(key(token))
43
+ end
44
+
45
+ def clear!(pattern = nil)
46
+ client.keys("#{@namespace}::#{pattern || '*'}").each do |key|
47
+ client.del(key)
48
+ end
49
+ end
50
+
51
+ def key(token)
52
+ [@namespace, token].compact.join('::')
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,83 @@
1
+ module Shrinker
2
+ class Config
3
+
4
+ # provides a getter and a setter with the same method
5
+ # config_setting :dog
6
+ # --> dog 'Husky' :=> 'Husky' (setter)
7
+ # --> dog :=> 'Husky' (getter)
8
+
9
+ def self.settings
10
+ @settings ||= []
11
+ end
12
+
13
+ def self.config_setting(name)
14
+ settings << name
15
+
16
+ class_eval <<-EV, __FILE__, __LINE__ + 1
17
+ attr_reader :#{name}
18
+
19
+ def #{name}(value = nil)
20
+ return @#{name} if value.nil?
21
+ @#{name} = value
22
+ value
23
+ end
24
+ EV
25
+ end
26
+
27
+ # the backend to be used to store the real url
28
+ config_setting :backend
29
+
30
+ # the options to pass to the backend
31
+ config_setting :backend_options
32
+
33
+ # domain to be shrinked can be a regex
34
+ config_setting :expanded_pattern
35
+
36
+ # regex for the url path to be excluded
37
+ config_setting :exclude_path
38
+
39
+ # regex to mactch/exclude the pattern when matching around patterns
40
+ config_setting :around_pattern
41
+
42
+ # domain to be used when shrinking the urls
43
+ config_setting :shrinked_pattern
44
+
45
+ # setting boolean to replace links only in anchor tags href inside html
46
+ config_setting :anchors_only_in_html
47
+
48
+ def ==(config)
49
+ self.class.settings.each { |setting| send(setting) == config.send(setting) }
50
+ end
51
+
52
+ def reset!
53
+ self.instance_variables.each do |var|
54
+ self.instance_variable_set(var, nil)
55
+ end
56
+ end
57
+
58
+ def merge(config)
59
+ new_config = Config.new
60
+ new_config.merge!(config)
61
+
62
+ new_config
63
+ end
64
+
65
+ def merge!(config)
66
+ case config
67
+ when self.class
68
+ self.class.settings.each { |setting| send(setting, config.send(setting)) }
69
+ when Hash
70
+ config.each_pair do |setting, value|
71
+ send(setting, value)
72
+ end
73
+ end
74
+ end
75
+
76
+ def backend_instance
77
+ @backend_instance ||= begin
78
+ class_name = (backend || 'Abstract').to_s
79
+ ::Shrinker::Backend.const_get(class_name).new(backend_options || {})
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,36 @@
1
+ module Shrinker
2
+ class EasyUrl < Struct.new(:url, :attributes)
3
+ require 'cgi'
4
+
5
+ def =~(regexp)
6
+ to_s =~ regexp
7
+ end
8
+
9
+ def to_s
10
+ parsed_uri.query = to_param
11
+ parsed_uri.query = nil if parsed_uri.query == ''
12
+ parsed_uri.to_s
13
+ end
14
+
15
+ def params
16
+ @params ||= begin
17
+ hash = {}
18
+ CGI.parse(parsed_uri.query).each_pair do |key, value|
19
+ hash[key.to_sym] = value.length == 1 ? value.first : value
20
+ end if parsed_uri.query
21
+ hash
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def to_param
28
+ return params.to_param('') if params.respond_to?(:to_param)
29
+ params.collect { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" }.join("&")
30
+ end
31
+
32
+ def parsed_uri
33
+ @parsed_uri ||= URI.parse(url)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module Shrinker
2
+ class Extractor < Struct.new(:token, :config)
3
+ def self.unshrink(token, config = nil)
4
+ self.new(token, config).unshrink
5
+ end
6
+
7
+ def unshrink
8
+ stored_content = backend.fetch(token)
9
+ raise UrlNotFound.new("Cannot find the url with token: #{token}") if stored_content == {}
10
+
11
+ EasyUrl.new(stored_content['url'], stored_content['attributes'] || {})
12
+ end
13
+
14
+ protected
15
+
16
+ def backend
17
+ config.backend_instance
18
+ end
19
+
20
+ def config
21
+ super || Shrinker.config
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ module Shrinker
2
+ module Parser
3
+ class Abstract < Struct.new(:content, :attributes)
4
+ attr_reader :config
5
+
6
+ # Delegate replace to an instance
7
+ def self.replace(content, attributes = {}, config = nil)
8
+ self.new(content, attributes, config).replace
9
+ end
10
+
11
+ def initialize(content = nil, attributes = nil, config = nil)
12
+ super(content, attributes)
13
+ merge_config(config)
14
+ end
15
+
16
+ def replace
17
+ raise "Shrinker::Parser::Abstract.replace not implemented"
18
+ end
19
+
20
+ protected
21
+
22
+ def url_regex
23
+ return base_url_regex if around_pattern.to_s == ''
24
+
25
+ Regexp.new around_pattern.to_s.gsub('$url', base_url_regex.to_s)
26
+ end
27
+
28
+ def base_url_regex
29
+ /(#{expanded_pattern}[-A-Z0-9+&@#\/%?=~_|$!:,.;]*[-A-Z0-9+&@#\/%=~_|$])/i
30
+ end
31
+
32
+ def excluded_path_regex
33
+ config.exclude_path
34
+ end
35
+
36
+ def expanded_pattern
37
+ config.expanded_pattern
38
+ end
39
+
40
+ def around_pattern
41
+ config.around_pattern
42
+ end
43
+
44
+ def shrinked_pattern
45
+ config.shrinked_pattern
46
+ end
47
+
48
+ def backend
49
+ config.backend_instance
50
+ end
51
+
52
+ def merge_config(config)
53
+ @config ||= Shrinker.config.dup
54
+ @config.merge!(config)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ module Shrinker
2
+ module Parser
3
+ class Mime < Abstract
4
+ require 'mail'
5
+
6
+ def replace
7
+ parts = email.all_parts.empty? ? [email] : email.all_parts
8
+
9
+ parts.each do |part|
10
+ new_body = replace_part_body(part)
11
+ part.body = if part.content_transfer_encoding
12
+ Mail::Body.new(new_body).encoded(part.content_transfer_encoding)
13
+ else
14
+ new_body.to_s
15
+ end
16
+ end
17
+ email.to_s
18
+ end
19
+
20
+ def email
21
+ @email ||= Mail.read_from_string(content)
22
+ end
23
+
24
+ def replace_part_body(part)
25
+ replace_config = config.dup
26
+ if part.mime_type == "text/html" && anchors_only_in_html?
27
+ replace_config.merge!({:around_pattern => anchor_tag_around_regex})
28
+ end
29
+
30
+ Text::replace(part.body.decoded, attributes, replace_config)
31
+ end
32
+
33
+ private
34
+
35
+ def anchor_tag_around_regex
36
+ /href="(?:https?:\/\/)?($url)"/
37
+ end
38
+
39
+ def anchors_only_in_html?
40
+ config.anchors_only_in_html == true
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ module Shrinker
2
+ module Parser
3
+ class Text < Abstract
4
+ def replace
5
+ content.gsub(url_regex) do |url|
6
+ matched_url = $1
7
+
8
+ next if matched_url =~ excluded_path_regex
9
+
10
+ url.gsub!(matched_url, shrink_url(matched_url))
11
+ end
12
+ end
13
+
14
+ protected
15
+
16
+ def shrink_url(url)
17
+ Url::replace(url, attributes, config)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module Shrinker
2
+ module Parser
3
+ class Url < Abstract
4
+ def replace
5
+ return url.to_s if url.params.delete(:shrinker).to_s == 'false'
6
+ return url.to_s if url =~ excluded_path_regex
7
+
8
+ new_url = url.to_s
9
+ new_url = shrink if new_url =~ url_regex
10
+
11
+ new_url
12
+ end
13
+
14
+ def shrink
15
+ token = Token.fetch_unique_token(backend, prefix: "__#{attributes.to_json}__#{content}")
16
+ backend.store(content, token, attributes)
17
+
18
+ if shrinked_pattern.respond_to?(:call)
19
+ if match = url.to_s.match(expanded_pattern)
20
+ start = shrinked_pattern.call(match)
21
+ else
22
+ # just bail out
23
+ return url.to_s
24
+ end
25
+ else
26
+ start = shrinked_pattern.to_s
27
+ end
28
+
29
+ [start, token].join("/")
30
+ end
31
+
32
+ def url
33
+ @url ||= EasyUrl.new(content)
34
+ end
35
+
36
+ def url_regex
37
+ base_url_regex
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ module Shrinker
2
+ module Serializer
3
+ extend self
4
+
5
+ def to_json(raw_url, attributes = {})
6
+ to_hash(raw_url, attributes).to_json
7
+ end
8
+
9
+ def to_hash(raw_url, attributes = {})
10
+ {:url => raw_url, :attributes => (attributes || {})}
11
+ end
12
+
13
+ # symbolize keys from the json if with_indifferent_access exists
14
+ def from_json(content)
15
+ hash = JSON.parse(content)
16
+ hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
17
+ hash
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module Shrinker
2
+ module Token
3
+ require 'digest/md5'
4
+
5
+ extend self
6
+
7
+ def fetch_unique_token(backend, options = {})
8
+ need_token = true
9
+
10
+ while need_token
11
+ token = generate(options)
12
+ need_token = backend.used_token?(token)
13
+ end
14
+
15
+ token
16
+ end
17
+
18
+ def generate(options = {})
19
+ prefix = options[:prefix]
20
+ Digest::MD5.hexdigest("#{prefix}__#{rand(99999)}")[-12..-1]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ module Shrinker
2
+ module VERSION
3
+
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ PATCH = 0
7
+ PRE = nil
8
+
9
+ def self.to_s
10
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
11
+ end
12
+ end
13
+ end
data/lib/shrinker.rb ADDED
@@ -0,0 +1,48 @@
1
+ require "shrinker/version"
2
+ require "json"
3
+
4
+ module Shrinker
5
+ autoload :Config, 'shrinker/config'
6
+ autoload :Serializer, 'shrinker/serializer'
7
+ autoload :Extractor, 'shrinker/extractor'
8
+ autoload :EasyUrl, 'shrinker/easy_url'
9
+ autoload :Token, 'shrinker/token'
10
+
11
+ module Backend
12
+ autoload :Abstract, 'shrinker/backend/abstract'
13
+ autoload :Redis, 'shrinker/backend/redis'
14
+ end
15
+
16
+ module Parser
17
+ autoload :Abstract, 'shrinker/parser/abstract'
18
+ autoload :Url, 'shrinker/parser/url'
19
+ autoload :Text, 'shrinker/parser/text'
20
+ autoload :Mime, 'shrinker/parser/mime'
21
+ end
22
+
23
+ class UrlNotFound < ArgumentError; end;
24
+
25
+ class << self
26
+ def configure(&block)
27
+ if block_given?
28
+ configuration.instance_eval(&block)
29
+ end
30
+ configuration
31
+ end
32
+ alias_method :config, :configure
33
+
34
+ def unshrink(token)
35
+ Shrinker::Extractor::unshrink(token, config)
36
+ end
37
+
38
+ protected
39
+
40
+ def configuration
41
+ @configuration ||= ::Shrinker::Config.new
42
+ end
43
+
44
+ def backend
45
+ config.backend_instance
46
+ end
47
+ end
48
+ end
data/shrinker.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'shrinker/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "shrinker"
8
+ gem.version = Shrinker::VERSION
9
+ gem.authors = ["jrichardlai"]
10
+ gem.email = ["jrichardlai@gmail.com"]
11
+ gem.description = %q{Find links in a text and shorten them}
12
+ gem.summary = %q{Find links in a text and shorten them}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency('json')
20
+ end