short 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
- :edit: the version ^ there is an oldy but a goody. There's been a lot of feature creep since then which would muck up a demo. Very soon there will be a way to disable said feature creep and we'll get a new demo.
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
- ### Configuration
12
+ ### Installation
11
13
 
12
- Currently, short uses either environment variables or settings from a config file, ~/.shortener.
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
- :SHORTENER_URL: http://< your shortener url>
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
- checkout `lib/shortener/configuration.rb` for more options that you can set.
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
- I'm going to put together a better way of handling this soon, because right now
23
- it's kind of a mess.
82
+ * shorten
83
+ * index
84
+ * fetch
85
+ * delete
24
86
 
25
- ### Usage
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 <jake AT jakewilkins DOT com>
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/client'
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::Client.new.shorten(url)['url']
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::Client.new.fetch(url)
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::Client.new.delete(short)
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::Client.new.index
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
- @options = Hash.new
21
- check_dotfile
22
- check_env
23
- @options = @options.merge!(opts)
24
- @options[:DEFAULT_URL] ||= '/index'
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?
@@ -1,5 +1,6 @@
1
1
  #link_display.row
2
- %div.offset5.span4
2
+ - classs = @data['s3'].nil? ? 'offset5' : ''
3
+ %div.span4{class: classs}
3
4
  %h4= @url
4
5
  %div.span2
5
6
  = clippy(@url)
@@ -16,7 +16,7 @@
16
16
  %a{href: '/index'} Index
17
17
  %li
18
18
  %a{href: '/add'} Add
19
- - if $conf.s3_enabled
19
+ - if $conf.s3_available
20
20
  %li
21
21
  %a{href: '/upload'} Upload
22
22
 
@@ -63,7 +63,7 @@
63
63
  // async: false,
64
64
  // });
65
65
  $.post('/upload.json', values, function(data){
66
- $('#shortener-display').text(data.url)
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
- %label.offset8.hide{id: 'shortener-display'}
105
+ #shortener-display.offset8.hide
106
106
  .field
107
107
  %label{for: 'shorterner[name]'} Name
108
108
  %input{type: 'text', name: 'shortener[name]'}
@@ -22,7 +22,7 @@ class Shortener
22
22
  $redis = Redis::Namespace.new(:shortener, redis: _redis)
23
23
  end
24
24
 
25
- set(:s3_enabled) {|v| condition {$conf.s3_enabled == v}}
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
- id = check_cache(url)
78
+ data = check_cache(url)
79
79
  end
80
- id ||= shorten(url, params)
80
+ data ||= shorten(url, params)
81
81
 
82
- return "#{base_url}/#{id}", id
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
- extras = ['url', url, 's3', true, 'shortened', key, 'extension', ext, 'set-count', 1]
94
- data = params.keys.map {|k| [k, params[k]] }.flatten.concat(extras)
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
- "#{base_url}/#{key}"
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 options['desired-short']
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 = ['shortened', key, 'url', url, 'set-count', 1]
140
- hsh_data.concat(['max-clicks', options['max-clicks'].to_i]) if options['max-clicks']
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.concat(['expire', ttl_key])
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
- key
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['shortened']
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, short: id}.to_json
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', s3_enabled: true do
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
- short = set_upload_short(params['shortener'])
298
- puts "set #{short} to #{params['shortener']['file_name']}"
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
- {url: short}.to_json
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
- @url, id = set_or_fetch_url(params["shortener"])
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, short: id, url: @url, html: haml(:display, :layout => false)}.to_json
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'
@@ -1,6 +1,6 @@
1
1
 
2
2
  class Shortener
3
3
 
4
- VERSION = '0.3.3'
4
+ VERSION = '0.4.0'
5
5
 
6
6
  end
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"
@@ -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.3.3
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: 2011-12-30 00:00:00.000000000 Z
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: &70233398587560 !ruby/object:Gem::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: *70233398587560
24
+ version_requirements: *70301329586160
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis-namespace
27
- requirement: &70233398586580 !ruby/object:Gem::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: *70233398586580
35
+ version_requirements: *70301329585240
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: haml
38
- requirement: &70233398584580 !ruby/object:Gem::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: *70233398584580
46
+ version_requirements: *70301329583820
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: turn
49
- requirement: &70233398580400 !ruby/object:Gem::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: *70233398580400
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
@@ -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