shrinker 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.
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