short 0.3.3 → 0.4.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/Guardfile +14 -0
- data/README.md +79 -11
- data/bin/short +8 -5
- data/lib/shortener/configuration.rb +41 -5
- data/lib/shortener/server/views/display.haml +2 -1
- data/lib/shortener/server/views/layout.haml +1 -1
- data/lib/shortener/server/views/upload.haml +2 -2
- data/lib/shortener/server.rb +29 -23
- data/lib/shortener/short.rb +147 -0
- data/lib/shortener/version.rb +1 -1
- data/lib/shortener.rb +5 -2
- data/tasks/heroku.rake +1 -1
- data/test/test_short.rb +68 -0
- metadata +14 -13
- data/lib/shortener/client.rb +0 -69
- data/test/test_client.rb +0 -26
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'minitest' do
|
5
|
+
# with Minitest::Unit
|
6
|
+
watch(%r|^test/test_(.*)\.rb|)
|
7
|
+
watch(%r|^lib/shortener/(.*)\.rb|) { |m| "test/test_#{m[1]}.rb" }
|
8
|
+
watch(%r|^test/test_helper\.rb|) { "test" }
|
9
|
+
|
10
|
+
# with Minitest::Spec
|
11
|
+
# watch(%r|^spec/(.*)_spec\.rb|)
|
12
|
+
# watch(%r|^lib/(.*)\.rb|) { |m| "spec/#{m[1]}_spec.rb" }
|
13
|
+
# watch(%r|^spec/spec_helper\.rb|) { "spec" }
|
14
|
+
end
|
data/README.md
CHANGED
@@ -5,24 +5,89 @@ A super simple, Sinatra based, Redis backed URL shortener designed to be deploye
|
|
5
5
|
|
6
6
|
Check it out [here](http://shortener1.heroku.com), but be aware that the css on the add page that displays the shortened link assumes you're using a short url, so it kind of looks like shit when it displays `http://shortener1.heroku.com/whatev`
|
7
7
|
|
8
|
-
|
8
|
+
for obvious reasons, the demo is configured with S3 disabled. However, if you are just looking
|
9
|
+
to play around with `short` follow the instructions below and use `http://shortener1.heroku.com`
|
10
|
+
as your the url you want to use.
|
9
11
|
|
10
|
-
###
|
12
|
+
### Installation
|
11
13
|
|
12
|
-
|
14
|
+
is now as easy as
|
15
|
+
|
16
|
+
`gem install short`
|
17
|
+
|
18
|
+
and
|
19
|
+
|
20
|
+
`short`
|
21
|
+
|
22
|
+
which will then prompt you to supply the config vars necessary to use the short
|
23
|
+
executable or the short server. You can check out `/lib/shortener/configuration.rb`
|
24
|
+
to see a list of available configuration variables.
|
25
|
+
|
26
|
+
configuration variables are parsed by default on-load and cached for each use,
|
27
|
+
but all client methods allow override.
|
28
|
+
|
29
|
+
### Server
|
30
|
+
|
31
|
+
Shortener server provides a primitive API for interacting with shorts. You get the following.
|
13
32
|
|
14
|
-
for the `short` client to work, you only need something like
|
15
33
|
<pre>
|
16
|
-
|
17
|
-
|
34
|
+
get '/index.json' => info on all shorts.
|
35
|
+
|
36
|
+
get '/delete/:id.json' => delete short @ id, returns {success: true, shortened: id}
|
37
|
+
|
38
|
+
get '/:id.json' => data hash for short @ id
|
39
|
+
|
40
|
+
post '/add.json' => data hash for new short
|
41
|
+
can accept the following options:
|
42
|
+
url: the url to shorten
|
43
|
+
expire: time in seconds that this short should live
|
44
|
+
max-clicks: the maximum number of clicks this short should accept.
|
45
|
+
desired-short: a short that should be set for this url.
|
46
|
+
allow-override: if desired-short is passed, whether or not to allow
|
47
|
+
a random short override.
|
48
|
+
|
49
|
+
post '/upload.json' => data hash for new short,
|
50
|
+
|
51
|
+
a data hash will contain the following keys:
|
52
|
+
|
53
|
+
shortened => id of this short. i.e. 'xZ147'
|
54
|
+
url => url of this short. i.e. 'www.google.com'
|
55
|
+
set-count => number of times this url has been shortened.
|
56
|
+
click-count => number of times this short has been resolved.
|
57
|
+
|
58
|
+
a data hash might contain the following keys:
|
59
|
+
|
60
|
+
expire => expire key that will be checked to see if this key will expire
|
61
|
+
max-clicks => maximum number of clicks this short will resolve for
|
62
|
+
|
63
|
+
if it's an endpoint that performs an action it will have a success key set to true or false.
|
64
|
+
(right now this is stupid and is set to true always, unless it errors, in which case you
|
65
|
+
get a 500 server error. hopefully that changes with some better error messages.)
|
66
|
+
|
67
|
+
|
68
|
+
S3 Keys
|
69
|
+
|
70
|
+
S3 => true if this is S3 content
|
71
|
+
extension => the file extension of S3 content. i.e. 'm4v'
|
72
|
+
file\_name => the name of the file. i.e. '1234.m4v'
|
73
|
+
name => the descriptive name. i.e. 'Pandas'
|
74
|
+
description => the description. i.e. 'A panda sneezes'
|
18
75
|
</pre>
|
19
76
|
|
20
|
-
|
77
|
+
### Client
|
78
|
+
|
79
|
+
`Shortener::Short` provides methods to access the server. You can access it through
|
80
|
+
the `Shortener` class or directly through the `Shortener::Short` class. You get:
|
21
81
|
|
22
|
-
|
23
|
-
|
82
|
+
* shorten
|
83
|
+
* index
|
84
|
+
* fetch
|
85
|
+
* delete
|
24
86
|
|
25
|
-
|
87
|
+
methods, each of which will return a/n istance of the `Short` class which will
|
88
|
+
parse the data and provide some defaults and access to said data.
|
89
|
+
|
90
|
+
### Executable
|
26
91
|
|
27
92
|
Use the `short` executable to
|
28
93
|
|
@@ -44,12 +109,15 @@ Run `short rake -T` to see more info.
|
|
44
109
|
|
45
110
|
### License
|
46
111
|
|
112
|
+
Short makes use of a number of libraries, each of which has its own license.
|
113
|
+
|
114
|
+
Short uses the DWTFYWPL.
|
47
115
|
|
48
116
|
<pre>
|
49
117
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
50
118
|
Version 2, December 2004
|
51
119
|
|
52
|
-
Copyright (C) 20011 Jake Wilkins
|
120
|
+
Copyright (C) 20011 Jake Wilkins \<jake AT jakewilkins DOT com\>
|
53
121
|
|
54
122
|
Everyone is permitted to copy and distribute verbatim or modified
|
55
123
|
copies of this license document, and changing it is allowed as long
|
data/bin/short
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
3
3
|
|
4
|
-
require 'shortener
|
4
|
+
require 'shortener'
|
5
5
|
|
6
6
|
def start_web
|
7
7
|
begin
|
@@ -27,18 +27,19 @@ def start_web
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def shorten(url)
|
30
|
-
puts Shortener
|
30
|
+
puts Shortener.shorten(url).short_url
|
31
31
|
end
|
32
32
|
|
33
33
|
def fetch(url)
|
34
34
|
if url =~ /http:/
|
35
35
|
url = url[-6..-1]
|
36
36
|
end
|
37
|
-
fetched = Shortener
|
37
|
+
fetched = Shortener.fetch(url)
|
38
38
|
unless fetched['success'] == false
|
39
39
|
puts <<-EOF
|
40
40
|
short => #{fetched['shortened']}
|
41
41
|
url => #{fetched['url']}
|
42
|
+
short-url => #{fetched.short_url}
|
42
43
|
set-count => #{fetched['set-count']}
|
43
44
|
click-count => #{fetched['click-count']}
|
44
45
|
expired => #{fetched['expired']}
|
@@ -50,12 +51,12 @@ def fetch(url)
|
|
50
51
|
end
|
51
52
|
|
52
53
|
def delete(short)
|
53
|
-
del = Shortener
|
54
|
+
del = Shortener.delete(short)
|
54
55
|
puts "#{short} deleted" if del['success']
|
55
56
|
end
|
56
57
|
|
57
58
|
def show_index
|
58
|
-
index = Shortener
|
59
|
+
index = Shortener.index
|
59
60
|
short_summary = index.map do |v|
|
60
61
|
url = v['url'].length > 38 ? "#{v['url'][0..25]}...#{v['url'][-10..-1]}" : "#{v['url']}"
|
61
62
|
"#{v['shortened']} : #{url} #{v['type'].nil? ? '' : ('type: ' + v['type'])}"
|
@@ -151,6 +152,8 @@ when 'rake'
|
|
151
152
|
do_action(:rake, ARGV[1])
|
152
153
|
when 'delete'
|
153
154
|
do_action(:delete, ARGV[1])
|
155
|
+
when '-v', '--version'
|
156
|
+
puts "short version #{Shortener::VERSION}"
|
154
157
|
else
|
155
158
|
do_action(:shorten, ARGV[0])
|
156
159
|
end
|
@@ -5,6 +5,14 @@ class Shortener
|
|
5
5
|
# The class for storing Configuration Information
|
6
6
|
class Configuration
|
7
7
|
|
8
|
+
class << self
|
9
|
+
def current
|
10
|
+
@current_configuration ||= Configuration.new(conf_current_placeholder: nil)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :options
|
15
|
+
|
8
16
|
OPTIONS = [:SHORTENER_URL, :DEFAULT_URL, :REDISTOGO_URL, :S3_KEY_PREFIX,
|
9
17
|
:S3_ACCESS_KEY_ID, :S3_SECRET_ACCESS_KEY, :S3_DEFAULT_ACL, :S3_BUCKET,
|
10
18
|
:DOTFILE_PATH, :S3_ENABLED]
|
@@ -17,13 +25,19 @@ class Shortener
|
|
17
25
|
# priority goes dotfile < env < passed option
|
18
26
|
def initialize(opts = Hash.new)
|
19
27
|
# TODO check keys by calls opts.delete {|k| !OPTIONS.include?(k)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
28
|
+
unless opts.empty?
|
29
|
+
opts.delete(:conf_current_placeholder)
|
30
|
+
@options = Hash.new
|
31
|
+
check_dotfile
|
32
|
+
check_env
|
33
|
+
@options = @options.merge!(opts)
|
34
|
+
@options[:DEFAULT_URL] ||= '/index'
|
35
|
+
else
|
36
|
+
@options = Configuration.current.options
|
37
|
+
end
|
25
38
|
end
|
26
39
|
|
40
|
+
# return a URI for an endpoint based on this configuration
|
27
41
|
def uri_for(end_point, opts = nil)
|
28
42
|
if END_POINTS.include?(end_point.to_sym)
|
29
43
|
path = path_for(end_point, opts)
|
@@ -33,6 +47,7 @@ class Shortener
|
|
33
47
|
end
|
34
48
|
end
|
35
49
|
|
50
|
+
# return a string ENV's for command line use.
|
36
51
|
def to_params
|
37
52
|
ret = Array.new
|
38
53
|
@options.each {|k,v| ret << "#{k}=#{v}" unless HEROKU_IGNORE.include?(k)}
|
@@ -47,6 +62,7 @@ class Shortener
|
|
47
62
|
end
|
48
63
|
end
|
49
64
|
|
65
|
+
# return the URI for the redistogo url
|
50
66
|
def redistogo_url
|
51
67
|
begin
|
52
68
|
URI.parse(@options[:REDISTOGO_URL])
|
@@ -58,10 +74,27 @@ class Shortener
|
|
58
74
|
end
|
59
75
|
end
|
60
76
|
|
77
|
+
# return a boolean of the S3_ENABLED option
|
61
78
|
def s3_enabled
|
62
79
|
@options[:S3_ENABLED].to_s == 'true'
|
63
80
|
end
|
64
81
|
|
82
|
+
# are the necessary options present for S3 to work?
|
83
|
+
def s3_configured
|
84
|
+
ret = true
|
85
|
+
[:S3_KEY_PREFIX, :S3_ACCESS_KEY_ID, :S3_SECRET_ACCESS_KEY,
|
86
|
+
:S3_DEFAULT_ACL, :S3_BUCKET ].each do |k|
|
87
|
+
ret = !@options[k].nil? unless ret == false
|
88
|
+
end
|
89
|
+
ret
|
90
|
+
end
|
91
|
+
|
92
|
+
# is S3 enabled and configured?
|
93
|
+
def s3_available
|
94
|
+
s3_enabled && s3_configured
|
95
|
+
end
|
96
|
+
|
97
|
+
# build an S3 policy.
|
65
98
|
def s3_policy
|
66
99
|
expiration_date = (Time.now + 36000).utc.strftime('%Y-%m-%dT%H:%M:%S.000Z') # 10.hours.from_now
|
67
100
|
max_filesize = 2147483648 # 2.gigabyte
|
@@ -79,6 +112,7 @@ class Shortener
|
|
79
112
|
).gsub(/\n|\r/, '')
|
80
113
|
end
|
81
114
|
|
115
|
+
# Sign an S3 policy
|
82
116
|
def s3_signature(policy)
|
83
117
|
signature = Base64.encode64(OpenSSL::HMAC.digest(
|
84
118
|
OpenSSL::Digest::Digest.new('sha1'), s3_secret_access_key, policy)).gsub("\n","")
|
@@ -86,6 +120,7 @@ class Shortener
|
|
86
120
|
|
87
121
|
private
|
88
122
|
|
123
|
+
# Parse the YAML dotfile if one exists
|
89
124
|
def check_dotfile
|
90
125
|
dotfile = @options[:DOTFILE_PATH] || File.join(ENV['HOME'], ".shortener")
|
91
126
|
if File.exists?(dotfile)
|
@@ -93,6 +128,7 @@ class Shortener
|
|
93
128
|
end
|
94
129
|
end
|
95
130
|
|
131
|
+
# Check our environment for any overrides.
|
96
132
|
def check_env
|
97
133
|
OPTIONS.each do |opt|
|
98
134
|
@options[opt] = ENV[opt.to_s] unless ENV[opt.to_s].nil?
|
@@ -63,7 +63,7 @@
|
|
63
63
|
// async: false,
|
64
64
|
// });
|
65
65
|
$.post('/upload.json', values, function(data){
|
66
|
-
$('#shortener-display').
|
66
|
+
$('#shortener-display').html(data.html)
|
67
67
|
.removeClass('hide');
|
68
68
|
});
|
69
69
|
|
@@ -102,7 +102,7 @@
|
|
102
102
|
%li.radio
|
103
103
|
%input{name: 'shortener[type]', type: 'radio', value: 'download'} Download
|
104
104
|
.row
|
105
|
-
|
105
|
+
#shortener-display.offset8.hide
|
106
106
|
.field
|
107
107
|
%label{for: 'shorterner[name]'} Name
|
108
108
|
%input{type: 'text', name: 'shortener[name]'}
|
data/lib/shortener/server.rb
CHANGED
@@ -22,7 +22,7 @@ class Shortener
|
|
22
22
|
$redis = Redis::Namespace.new(:shortener, redis: _redis)
|
23
23
|
end
|
24
24
|
|
25
|
-
set(:
|
25
|
+
set(:s3_available) {|v| condition {$conf.s3_available == v}}
|
26
26
|
|
27
27
|
helpers do
|
28
28
|
|
@@ -75,11 +75,11 @@ class Shortener
|
|
75
75
|
end
|
76
76
|
|
77
77
|
unless params['max-clicks'] || params['expire'] || params['desired-short']
|
78
|
-
|
78
|
+
data = check_cache(url)
|
79
79
|
end
|
80
|
-
|
80
|
+
data ||= shorten(url, params)
|
81
81
|
|
82
|
-
|
82
|
+
data
|
83
83
|
end
|
84
84
|
|
85
85
|
def set_upload_short(params)
|
@@ -90,13 +90,14 @@ class Shortener
|
|
90
90
|
hash_key = "data:#{sha}:#{key}"
|
91
91
|
url = "https://s3.amazonaws.com/#{$conf.s3_bucket}/#{$conf.s3_key_prefix}/#{fname}"
|
92
92
|
ext = File.extname(fname)[1..-1]
|
93
|
-
|
94
|
-
|
93
|
+
data = {'url' => url, 's3' => true, 'shortened' => key,
|
94
|
+
'extension' => ext, 'set-count' => 1}
|
95
|
+
data = params.merge(data)
|
95
96
|
|
96
97
|
$redis.set(key, sha)
|
97
|
-
$redis.hmset(hash_key, *data)
|
98
|
+
$redis.hmset(hash_key, *arrayify_hash(data))
|
98
99
|
|
99
|
-
|
100
|
+
data
|
100
101
|
end
|
101
102
|
|
102
103
|
def shorten(url, options = {})
|
@@ -116,7 +117,7 @@ class Shortener
|
|
116
117
|
if (!prev_set['max-clicks'] && !prev_set['expire'] &&
|
117
118
|
(prev_set['url'] == url.to_s))
|
118
119
|
$redis.hincrby(check_key, 'set-count', 1)
|
119
|
-
return
|
120
|
+
return prev_set
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
@@ -136,19 +137,19 @@ class Shortener
|
|
136
137
|
sha = Digest::SHA1.hexdigest(url.to_s)
|
137
138
|
$redis.set(key, sha)
|
138
139
|
|
139
|
-
hsh_data =
|
140
|
-
hsh_data
|
140
|
+
hsh_data = {'shortened' => key, 'url' => url, 'set-count' => 1}
|
141
|
+
hsh_data['max-clicks'] = options['max-clicks'].to_i if options['max-clicks']
|
141
142
|
|
142
143
|
if options['expire'] # set expire time if specified
|
143
144
|
ttl = options['expire'].to_i
|
144
145
|
ttl_key = "expire:#{sha}:#{key}"
|
145
146
|
$redis.set(ttl_key, "#{sha}:#{key}")
|
146
147
|
$redis.expire(ttl_key, ttl)
|
147
|
-
hsh_data
|
148
|
+
hsh_data[:expire] = ttl_key
|
148
149
|
end
|
149
|
-
$redis.hmset("data:#{sha}:#{key}", *hsh_data)
|
150
|
+
$redis.hmset("data:#{sha}:#{key}", *arrayify_hash(hsh_data))
|
150
151
|
|
151
|
-
|
152
|
+
hsh_data
|
152
153
|
end
|
153
154
|
|
154
155
|
def check_cache(url)
|
@@ -158,7 +159,7 @@ class Shortener
|
|
158
159
|
short = $redis.hgetall(key)
|
159
160
|
unless short == {} || short['expire'] || short['max-clicks']
|
160
161
|
$redis.hincrby(key, 'set-count', 1)
|
161
|
-
return short
|
162
|
+
return short
|
162
163
|
end
|
163
164
|
end
|
164
165
|
nil
|
@@ -194,6 +195,10 @@ class Shortener
|
|
194
195
|
ret
|
195
196
|
end
|
196
197
|
|
198
|
+
def arrayify_hash(hsh)
|
199
|
+
hsh.keys.map {|k| [k, hsh[k]] }.flatten
|
200
|
+
end
|
201
|
+
|
197
202
|
end
|
198
203
|
|
199
204
|
before do
|
@@ -224,17 +229,16 @@ class Shortener
|
|
224
229
|
end
|
225
230
|
|
226
231
|
get '/delete/:id.:format' do |id, format|
|
227
|
-
puts "#{id}, #{format}"
|
228
232
|
delete_short(id)
|
229
233
|
if format == 'json'
|
230
234
|
content_type :json
|
231
|
-
{success: true,
|
235
|
+
{success: true, shortened: id}.to_json
|
232
236
|
else
|
233
237
|
redirect :index
|
234
238
|
end
|
235
239
|
end
|
236
240
|
|
237
|
-
get '/upload',
|
241
|
+
get '/upload', s3_available: true do
|
238
242
|
policy = $conf.s3_policy
|
239
243
|
signature = $conf.s3_signature(policy)
|
240
244
|
|
@@ -294,11 +298,12 @@ class Shortener
|
|
294
298
|
end
|
295
299
|
|
296
300
|
post '/upload.?:format?' do |format|
|
297
|
-
|
298
|
-
puts "set #{
|
301
|
+
@data = set_upload_short(params['shortener'])
|
302
|
+
puts "set #{@data['shortened']} to #{params['shortener']['file_name']}"
|
303
|
+
@url = "#{base_url}/#{@data['shortened']}"
|
299
304
|
if format == 'json'
|
300
305
|
content_type :json
|
301
|
-
{
|
306
|
+
@data.merge({html: haml(:display, layout: false)}).to_json
|
302
307
|
else
|
303
308
|
redirect :index
|
304
309
|
end
|
@@ -312,11 +317,12 @@ class Shortener
|
|
312
317
|
# essentally, params = params
|
313
318
|
end
|
314
319
|
|
315
|
-
@
|
320
|
+
@data = set_or_fetch_url(params["shortener"])
|
321
|
+
@url = "#{base_url}/#{@data['shortened']}"
|
316
322
|
puts "set #{@url} to #{params['shortener']['url']}"
|
317
323
|
if format == 'json'
|
318
324
|
content_type :json
|
319
|
-
{success: true,
|
325
|
+
@data.merge({success: true, html: haml(:display, :layout => false)}).to_json
|
320
326
|
else
|
321
327
|
haml :display
|
322
328
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
|
2
|
+
class Shortener
|
3
|
+
class Short
|
4
|
+
|
5
|
+
SHORT_KEYS = [:url, :shortened, :type, :ext, :s3, :'click-count', :'max-count',
|
6
|
+
:'set-count', :'expire-time', :sha]
|
7
|
+
|
8
|
+
attr_reader :data
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# set a shortened url
|
13
|
+
def shorten(url, conf = nil)
|
14
|
+
opts = {'shortener' => {'url' => url}.to_json}
|
15
|
+
response = request(:post, :add, conf, opts)
|
16
|
+
if response.is_a?(Net::HTTPOK)
|
17
|
+
return Short.new(response.body, conf)
|
18
|
+
else
|
19
|
+
raise "OH SHIT! #{response}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# get data for a short, including full url.
|
24
|
+
def fetch(short, conf = nil)
|
25
|
+
response = request(:get, :fetch, conf, short)
|
26
|
+
Short.new(response.body, conf)
|
27
|
+
end
|
28
|
+
|
29
|
+
# post a file to the configured s3 bucket and set a short.
|
30
|
+
def upload(file)
|
31
|
+
end
|
32
|
+
|
33
|
+
# fetch data on multiple shorts
|
34
|
+
def index(start = 0, stop = nil, conf = nil)
|
35
|
+
response = request(:get, :index, conf)
|
36
|
+
data = JSON.parse(response.body)
|
37
|
+
shorts = data.map {|sh| Short.new(sh, conf)}
|
38
|
+
shorts
|
39
|
+
end
|
40
|
+
|
41
|
+
# delete a short
|
42
|
+
def delete(short, conf = nil)
|
43
|
+
response = request(:get, :delete, conf, short)
|
44
|
+
Short.new(response.body, conf)
|
45
|
+
end
|
46
|
+
|
47
|
+
# build a request based on configurations
|
48
|
+
def request(type, end_point, conf = nil, args = nil)
|
49
|
+
config = conf || Shortener::Configuration.current
|
50
|
+
case type
|
51
|
+
when :post
|
52
|
+
Net::HTTP.post_form(config.uri_for(end_point), args)
|
53
|
+
when :get
|
54
|
+
Net::HTTP.get_response(config.uri_for(end_point, args))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end # => ClassMethods
|
59
|
+
|
60
|
+
class << self; include ClassMethods; end
|
61
|
+
|
62
|
+
# Set up this short. Will:
|
63
|
+
# * store the configuration if necessary
|
64
|
+
# * parse JSON if short is JSON
|
65
|
+
# * symbolize the shorts keys
|
66
|
+
# * turn string numbers in to actual numbers.
|
67
|
+
def initialize(short, conf = nil)
|
68
|
+
@configuration = conf unless conf.nil?
|
69
|
+
short = parse_return(short) unless short.is_a?(Hash)
|
70
|
+
@data = symbolize_keys(short)
|
71
|
+
normalize_data
|
72
|
+
end
|
73
|
+
|
74
|
+
# the configuration that this short will use.
|
75
|
+
def configuration
|
76
|
+
@configuation.nil? ? Shortener::Configuration.current : @configuration
|
77
|
+
end
|
78
|
+
|
79
|
+
# an alternative way to fetch stuff from @data
|
80
|
+
def [](key)
|
81
|
+
@data[key.to_sym]
|
82
|
+
end
|
83
|
+
|
84
|
+
# return a URI of the URL
|
85
|
+
def uri
|
86
|
+
URI.parse(url)
|
87
|
+
end
|
88
|
+
|
89
|
+
# shortened combined with config#shortener_url
|
90
|
+
def short_url
|
91
|
+
"#{configuration.shortener_url}/#{shortened}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# a URI of short url.
|
95
|
+
def short_uri
|
96
|
+
URI.parse(short_url)
|
97
|
+
end
|
98
|
+
|
99
|
+
# allow the updating of a field. considering an arity like:
|
100
|
+
# update(hsh_of_fields_with_values)
|
101
|
+
# and it will create a POST request to send server side.
|
102
|
+
def update
|
103
|
+
#TODO
|
104
|
+
end
|
105
|
+
|
106
|
+
SHORT_KEYS.each do |key|
|
107
|
+
name = key.to_s.include?('-') ? key.to_s.gsub('-', '_').to_sym : key
|
108
|
+
define_method name do
|
109
|
+
@data[key]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def pretty_print
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# used to turn the hash keys of the data hash in to symbols.
|
120
|
+
def symbolize_keys(hash)
|
121
|
+
ret = Hash.new
|
122
|
+
hash.each do |k,v|
|
123
|
+
ret[k.to_sym] = v
|
124
|
+
end
|
125
|
+
ret
|
126
|
+
end
|
127
|
+
|
128
|
+
# turn string numbers in to actual numbers.
|
129
|
+
def normalize_data
|
130
|
+
[:'click-count', :'max-count', :'set-count'].each do |k|
|
131
|
+
@data[k] = @data[k].to_i
|
132
|
+
end
|
133
|
+
@data[:'click-count'] ||= 0
|
134
|
+
end
|
135
|
+
|
136
|
+
# parse JSON safely.
|
137
|
+
def parse_return(json)
|
138
|
+
begin
|
139
|
+
return JSON.parse(json)
|
140
|
+
rescue Exception => boom
|
141
|
+
raise "OH SHIT! #{boom}\n\n #{json}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
require_relative '../shortener'
|
data/lib/shortener/version.rb
CHANGED
data/lib/shortener.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
1
4
|
dir = File.expand_path(File.dirname(__FILE__))
|
2
5
|
|
3
6
|
require File.join(dir, 'shortener', 'version')
|
4
|
-
require File.join(dir, 'shortener', 'client')
|
5
7
|
require File.join(dir, 'shortener', 'configuration')
|
8
|
+
require File.join(dir, 'shortener', 'short')
|
6
9
|
|
7
10
|
class Shortener
|
8
|
-
|
11
|
+
class << self; include Shortener::Short::ClassMethods; end
|
9
12
|
end
|
data/tasks/heroku.rake
CHANGED
@@ -22,7 +22,7 @@ namespace :heroku do
|
|
22
22
|
require_relative '../lib/shortener'
|
23
23
|
$name = ENV['APPNAME'] || "shner-#{`whoami`.chomp}"
|
24
24
|
cmd = Dir.pwd =~ /heroku$/ ? "" : "cd heroku && "
|
25
|
-
cmd += "heroku create #{name}"
|
25
|
+
cmd += "heroku create #{$name}"
|
26
26
|
cmd += " && heroku addons:add redistogo:nano"
|
27
27
|
cmd += " && heroku config:add #{Shortener::Configuration.new.to_params}"
|
28
28
|
cmd += " && heroku addons:add custom_domains:basic"
|
data/test/test_short.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require_relative '../lib/shortener/short'
|
3
|
+
|
4
|
+
class TestShortenerShort < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@short = Shortener::Short.new('url' => 'http://google.com', 'shortened' => '12345',
|
8
|
+
'set-count' => '12')
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_instance_methods
|
12
|
+
assert @short.shortened == '12345'
|
13
|
+
assert @short.url == 'http://google.com'
|
14
|
+
assert @short.set_count == 12
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_brackets
|
18
|
+
assert @short['shortened'] == '12345'
|
19
|
+
assert @short[:shortened] == '12345'
|
20
|
+
assert @short['set-count'] == 12
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_defaults_ensure_keys
|
24
|
+
short = Shortener.shorten('www.google.com')
|
25
|
+
assert !short.shortened.nil?
|
26
|
+
assert !short.url.nil?
|
27
|
+
assert !short.set_count.nil?
|
28
|
+
assert !short.click_count.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_short_url
|
32
|
+
assert @short.short_url == "#{@short.configuration.shortener_url}/#{@short.shortened}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_uri
|
36
|
+
assert @short.uri == URI.parse(@short.url)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_add
|
40
|
+
short = Shortener::Short.shorten('www.google.com')
|
41
|
+
assert short.is_a?(Shortener::Short)
|
42
|
+
assert short.shortened.nil? == false
|
43
|
+
assert short.url == 'http://www.google.com'
|
44
|
+
assert short['success']
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_index
|
48
|
+
ind = Shortener::Short.index
|
49
|
+
assert ind.is_a?(Array)
|
50
|
+
assert ind.length >= 1
|
51
|
+
assert ind.first.is_a?(Shortener::Short)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_fetch
|
55
|
+
short = Shortener.shorten('www.google.com')
|
56
|
+
short2 = Shortener::Short.fetch(short.shortened)
|
57
|
+
assert short.shortened == short2.shortened
|
58
|
+
short = Shortener.fetch('nope')
|
59
|
+
assert short[:success] == false
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_delete
|
63
|
+
add = Shortener.shorten('www.google.com')
|
64
|
+
del = Shortener.delete(add['shortened'])
|
65
|
+
assert del['success']
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: short
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-01-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
16
|
-
requirement: &
|
16
|
+
requirement: &70301329586160 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70301329586160
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: redis-namespace
|
27
|
-
requirement: &
|
27
|
+
requirement: &70301329585240 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70301329585240
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: haml
|
38
|
-
requirement: &
|
38
|
+
requirement: &70301329583820 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70301329583820
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: turn
|
49
|
-
requirement: &
|
49
|
+
requirement: &70301329582280 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70301329582280
|
58
58
|
description: A (hopefully) easy and handy deployable APIable way to shorten links.
|
59
59
|
email:
|
60
60
|
- jake@jakewilkins.com
|
@@ -64,12 +64,12 @@ extensions: []
|
|
64
64
|
extra_rdoc_files: []
|
65
65
|
files:
|
66
66
|
- .gitignore
|
67
|
+
- Guardfile
|
67
68
|
- README.md
|
68
69
|
- Rakefile
|
69
70
|
- bin/short
|
70
71
|
- config.ru
|
71
72
|
- lib/shortener.rb
|
72
|
-
- lib/shortener/client.rb
|
73
73
|
- lib/shortener/configuration.rb
|
74
74
|
- lib/shortener/server.rb
|
75
75
|
- lib/shortener/server/Gemfile
|
@@ -100,12 +100,13 @@ files:
|
|
100
100
|
- lib/shortener/server/views/s3/layout.haml
|
101
101
|
- lib/shortener/server/views/s3/video.haml
|
102
102
|
- lib/shortener/server/views/upload.haml
|
103
|
+
- lib/shortener/short.rb
|
103
104
|
- lib/shortener/version.rb
|
104
105
|
- short.gemspec
|
105
106
|
- tasks/heroku.rake
|
106
|
-
- test/test_client.rb
|
107
107
|
- test/test_configuration.rb
|
108
108
|
- test/test_server.rb
|
109
|
+
- test/test_short.rb
|
109
110
|
homepage: ''
|
110
111
|
licenses: []
|
111
112
|
post_install_message:
|
@@ -131,6 +132,6 @@ signing_key:
|
|
131
132
|
specification_version: 3
|
132
133
|
summary: A Link Shortener
|
133
134
|
test_files:
|
134
|
-
- test/test_client.rb
|
135
135
|
- test/test_configuration.rb
|
136
136
|
- test/test_server.rb
|
137
|
+
- test/test_short.rb
|
data/lib/shortener/client.rb
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
dir = File.expand_path(File.dirname(__FILE__))
|
2
|
-
require 'net/http'
|
3
|
-
require 'json'
|
4
|
-
require File.join(dir, 'configuration')
|
5
|
-
|
6
|
-
class Shortener
|
7
|
-
class Client
|
8
|
-
|
9
|
-
attr_accessor :configuration
|
10
|
-
|
11
|
-
def initialize(options = Hash.new)
|
12
|
-
@configuration = Configuration.new(options)
|
13
|
-
end
|
14
|
-
|
15
|
-
# set a shortened url
|
16
|
-
def shorten(url)
|
17
|
-
opts = {'shortener' => {'url' => url}.to_json}
|
18
|
-
response= request(:post, :add, opts)
|
19
|
-
if response.is_a?(Net::HTTPOK)
|
20
|
-
return parse_return(response.body)
|
21
|
-
else
|
22
|
-
raise "OH SHIT! #{response}"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# get data for a short, including full url.
|
27
|
-
def fetch(short)
|
28
|
-
response = request(:get, :fetch, short)
|
29
|
-
parse_return(response)
|
30
|
-
end
|
31
|
-
|
32
|
-
# post a file to the configured s3 bucket and set a short.
|
33
|
-
def upload(file)
|
34
|
-
end
|
35
|
-
|
36
|
-
# fetch data on multiple shorts
|
37
|
-
def index(start = 0, stop = nil)
|
38
|
-
response = request(:get, :index)
|
39
|
-
parse_return(response)
|
40
|
-
end
|
41
|
-
|
42
|
-
# delete a short
|
43
|
-
def delete(short)
|
44
|
-
response = request(:get, :delete, short)
|
45
|
-
parse_return(response)
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
# build a request based on configurations
|
51
|
-
def request(type, end_point, args = nil)
|
52
|
-
case type
|
53
|
-
when :post
|
54
|
-
Net::HTTP.post_form(@configuration.uri_for(end_point), args)
|
55
|
-
when :get
|
56
|
-
Net::HTTP.get(@configuration.uri_for(end_point, args))
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def parse_return(json)
|
61
|
-
begin
|
62
|
-
return JSON.parse(json)
|
63
|
-
rescue Exception => boom
|
64
|
-
raise "OH SHIT! #{boom}\n\n #{json}"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
end # => Client
|
69
|
-
end # => Shortener
|
data/test/test_client.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'minitest/autorun'
|
2
|
-
require_relative '../lib/shortener/client'
|
3
|
-
|
4
|
-
class TestShortenerClient < MiniTest::Unit::TestCase
|
5
|
-
|
6
|
-
def setup
|
7
|
-
@client = Shortener::Client.new
|
8
|
-
end
|
9
|
-
|
10
|
-
def test_add
|
11
|
-
short = @client.shorten('www.google.com')
|
12
|
-
assert short['success']
|
13
|
-
end
|
14
|
-
|
15
|
-
def test_index
|
16
|
-
ind = @client.index
|
17
|
-
assert ind.is_a?(Array)
|
18
|
-
end
|
19
|
-
|
20
|
-
def test_delete
|
21
|
-
add = @client.shorten('www.google.com')
|
22
|
-
del = @client.delete(add['short'])
|
23
|
-
assert del['success']
|
24
|
-
end
|
25
|
-
|
26
|
-
end
|