oso 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gems ADDED
@@ -0,0 +1,4 @@
1
+ new_base_60 --version '= 1.0.3'
2
+ rack --version '= 1.2.1'
3
+ redis-namespace --version '= 0.10.0'
4
+ sinatra --version '= 1.1.2'
data/.gemtest ADDED
File without changes
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ === 1.0.0 / 2011-02-15
2
+
3
+ * Birthday!
data/Manifest.txt ADDED
@@ -0,0 +1,17 @@
1
+ .gems
2
+ CHANGELOG.rdoc
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ config.ru
7
+ lib/oso.rb
8
+ lib/oso/public/audiosocket.png
9
+ lib/oso/public/favicon.ico
10
+ lib/oso/public/index.css
11
+ lib/oso/public/index.html
12
+ lib/oso/public/robots.txt
13
+ lib/oso/public/style.css
14
+ lib/oso/server.rb
15
+ lib/oso/views/layout.rhtml
16
+ lib/oso/views/stats.rhtml
17
+ test/test_oso.rb
data/README.rdoc ADDED
@@ -0,0 +1,94 @@
1
+ = Oso
2
+
3
+ * http://github.com/audiosocket/oso
4
+
5
+ == Description
6
+
7
+ Audiosocket's minimal URL shortener. It's named "oso" because those
8
+ three letters look a bit like our logo.
9
+
10
+ == Configuring
11
+
12
+ We deploy 1.9.2, so Oso expects it. It shouldn't be too hard to port
13
+ to 1.8 if you feel the need.
14
+
15
+ If you install the deps in the <tt>.gems</tt> file you can run
16
+ <tt>rackup</tt> by itself.
17
+
18
+ Oso needs Redis. Set the <tt>OSO_REDIS_URL</tt> environment variable,
19
+ which defaults to <tt>redis://localhost:6379</tt>. All keys are
20
+ namespaced under <tt>oso:</tt>.
21
+
22
+ == Using
23
+
24
+ There's a static HTML index that'll let you shorten URLs by hand, but
25
+ we mostly use Oso inside other apps. To shorten a URL, <tt>POST</tt>
26
+ to <tt>/</tt> with a <tt>url</tt> parameter. It'll return a
27
+ <tt>201</tt> with the shortened URL in the body.
28
+
29
+ You can optionally pass a <tt>life</tt> parameter, which is the
30
+ lifetime of the shortened URL in seconds.
31
+
32
+ You can optionally pass a <tt>limit</tt> parameter, which is the number
33
+ of times you want the shortened URL to work. After the limit is
34
+ reached <tt>404</tt>'s are returned.
35
+
36
+ You can optionally pass a <tt>name</tt> parameter, which will be used
37
+ instead of generating a token for the shortened URL. Names can only
38
+ contain alphanumerics and dashes.
39
+
40
+ === From Ruby
41
+
42
+ If you install the <tt>oso</tt> RubyGem:
43
+
44
+ require "oso"
45
+
46
+ Oso.shorten "github.com" # => "http://oso.local/Acf"
47
+ Oso.shorten "github.com", life: 30 # => "http://oso.local/Ad0"
48
+
49
+ Set the <tt>OSO_URL</tt> environment variable to control where the
50
+ client looks for the server, or see the RDocs for information on
51
+ creating multiple instances.
52
+
53
+ == Deploying
54
+
55
+ If you fork it, Oso is already prepped for Heroku. It has an
56
+ up-to-date <tt>.gems</tt> manifest, and it'll use the
57
+ <tt>REDISTOGO_URL</tt> env var if it exists.
58
+
59
+ If you'd like to use it in your own <tt>config.ru</tt> file via the
60
+ gem, you'll need to provide the server's dependencies yourself. The
61
+ gem doesn't express any runtime dependencies since it's primarily used
62
+ as a client.
63
+
64
+ Assuming you've provided the server dependencies (see the
65
+ <tt>.gems</tt> file), using the gem from Rack is simple:
66
+
67
+ require "oso/server"
68
+ run Sinatra::Application
69
+
70
+ Pull requests making Oso a mountable <tt>Sinatra::Base</tt> app are
71
+ most welcome.
72
+
73
+ == License
74
+
75
+ Copyright 2011 Leopona Inc.
76
+
77
+ Permission is hereby granted, free of charge, to any person obtaining
78
+ a copy of this software and associated documentation files (the
79
+ 'Software'), to deal in the Software without restriction, including
80
+ without limitation the rights to use, copy, modify, merge, publish,
81
+ distribute, sublicense, and/or sell copies of the Software, and to
82
+ permit persons to whom the Software is furnished to do so, subject to
83
+ the following conditions:
84
+
85
+ The above copyright notice and this permission notice shall be
86
+ included in all copies or substantial portions of the Software.
87
+
88
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
89
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
90
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
91
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
92
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
93
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
94
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "hoe"
2
+
3
+ Hoe.plugin :doofus, :git
4
+
5
+ Hoe.spec "oso" do
6
+ developer "Audiosocket", "it@audiosocket.com"
7
+
8
+ self.extra_rdoc_files = Dir["*.rdoc"]
9
+ self.history_file = "CHANGELOG.rdoc"
10
+ self.readme_file = "README.rdoc"
11
+ self.testlib = :minitest
12
+
13
+ extra_dev_deps << ["fakeweb", "1.3.0"]
14
+ extra_dev_deps << ["minitest", "2.0.2"]
15
+ extra_dev_deps << ["mocha", "0.9.12"]
16
+ end
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ # This load path manipulation is unnecessary if you're running Oso
2
+ # from the released gem.
3
+
4
+ $:.unshift "lib"
5
+ require "oso/server"
6
+
7
+ run Sinatra::Application
Binary file
File without changes
@@ -0,0 +1,55 @@
1
+ html {
2
+ background: #fff url(/audiosocket.png) no-repeat fixed center;
3
+ }
4
+
5
+ body {
6
+ font-family : "Helvetica Neue", sans-serif;
7
+ margin-top : 72px;
8
+ text-align : center;
9
+ }
10
+
11
+ input {
12
+ background : #fff;
13
+ border : 1px solid #000;
14
+ color : #000;
15
+ font-size : 18px;
16
+ margin : 0;
17
+ padding : 0;
18
+ }
19
+
20
+ #url {
21
+ border-bottom-left-radius : 4px;
22
+ border-top-left-radius : 4px;
23
+ padding : 6px;
24
+ width : 300px;
25
+ }
26
+
27
+ #shorten {
28
+ -webkit-transition : 0.3s;
29
+ background : #000;
30
+ border-bottom-right-radius : 4px;
31
+ border-top-right-radius : 4px;
32
+ color : #777;
33
+ cursor : pointer;
34
+ padding : 6px;
35
+ }
36
+
37
+ #shorten:hover {
38
+ color: #eee;
39
+ }
40
+
41
+ #shortened input {
42
+ border : 0;
43
+ color : #333;
44
+ cursor : text !important;
45
+ font-size : 24px;
46
+ text-align : center;
47
+ width : 500px;
48
+ }
49
+
50
+ #shortened p {
51
+ color : #666;
52
+ font-size : 13px;
53
+ margin-top : 6px;
54
+ }
55
+
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html>
4
+ <head>
5
+ <title>Oso</title>
6
+ <link href="/index.css" rel="stylesheet" type="text/css">
7
+ </head>
8
+
9
+ <body>
10
+ <form action="/" method="POST">
11
+ <input id="url" name="url" placeholder="http://example.org"
12
+ type="text"><input id="shorten" type="submit" value="Shorten">
13
+ </form>
14
+
15
+ <div id="shortened" style="display:none">
16
+ <input readonly>
17
+ <p>Copy and share this URL.</p>
18
+ </div>
19
+
20
+ <script type="text/javascript"
21
+ src="http://code.jquery.com/jquery-1.5.min.js"></script>
22
+
23
+ <script type="text/javascript">
24
+ $(function() {
25
+ $("form").submit(function() {
26
+ var url = $("#url").val()
27
+
28
+ if (!/^\s*$/.test(url)) {
29
+ $.post("/", { url: url }, function(short) {
30
+ $("form").hide()
31
+ $("#shortened").show()
32
+ $("#shortened input").val(short).select()
33
+ })
34
+ }
35
+
36
+ return false
37
+ })
38
+
39
+ $("#shortened").click(function() {
40
+ $("input", this).select()
41
+ })
42
+ })
43
+ </script>
44
+ </body>
45
+ </html>
@@ -0,0 +1,2 @@
1
+ User-Agent: *
2
+ Disallow: /
@@ -0,0 +1,94 @@
1
+ * {
2
+ font-size : 16px;
3
+ margin : 0;
4
+ padding : 0;
5
+ }
6
+
7
+ a {
8
+ color: #a00;
9
+ }
10
+
11
+ a.btn {
12
+ -webkit-transition : .2s;
13
+ background : #ddd;
14
+ border-radius : 4px;
15
+ color : #333;
16
+ display : block;
17
+ font-size : 14px;
18
+ font-weight : normal;
19
+ padding : 4px 6px;
20
+ text-decoration : none;
21
+ }
22
+
23
+ a.btn:hover {
24
+ background : #333;
25
+ color : #eee;
26
+ }
27
+
28
+ a.btn:active {
29
+ color: #f00;
30
+ }
31
+
32
+ body {
33
+ font-family : "Helvetica Neue", sans-serif;
34
+ width : 600px;
35
+ margin : 36px auto;
36
+ }
37
+
38
+ h1 {
39
+ border-bottom : 1px solid #ccc;
40
+ font-size : 36px;
41
+ margin-bottom : 18px;
42
+ padding-bottom : 10px;
43
+ position : relative;
44
+ }
45
+
46
+ h1 a {
47
+ bottom : 16px;
48
+ position : absolute;
49
+ right : 0;
50
+ }
51
+
52
+ h2 {
53
+ clear : left;
54
+ font-size : 24px;
55
+ margin-bottom : 18px;
56
+ padding-top : 36px;
57
+ position : relative;
58
+ }
59
+
60
+ table {
61
+ width: 100%;
62
+ }
63
+
64
+ thead th {
65
+ border-bottom : 1px solid #ccc;
66
+ text-align : left;
67
+ }
68
+
69
+ td, th {
70
+ padding: 3px;
71
+ }
72
+
73
+ .boxen li {
74
+ background : #000;
75
+ border-radius : 8px;
76
+ color : #aaa;
77
+ float : left;
78
+ font-size : 13px;
79
+ list-style-type : none;
80
+ margin-left : 18px;
81
+ padding : 12px;
82
+ text-align : center;
83
+ width : 164px;
84
+ }
85
+
86
+ .boxen li:first-child {
87
+ margin-left: 0;
88
+ }
89
+
90
+ .boxen li .num {
91
+ color : #eee;
92
+ display : block;
93
+ font-size : 24px;
94
+ }
data/lib/oso/server.rb ADDED
@@ -0,0 +1,107 @@
1
+ require "redis/namespace"
2
+ require "new_base_60"
3
+ require "sinatra"
4
+ require "uri"
5
+
6
+ url = ENV.values_at("OSO_REDIS_URL", "REDISTOGO_URL").compact.first
7
+ url = URI.parse url || "redis://localhost:6379"
8
+
9
+ $redis = Redis::Namespace.new :oso,
10
+ redis: Redis.new(host: url.host, password: url.password, port: url.port)
11
+
12
+ helpers do
13
+ def bad! message
14
+ halt 412, {}, message
15
+ end
16
+
17
+ def nope!
18
+ halt 404, {}, "No luck."
19
+ end
20
+
21
+ def save short, url, limit, life
22
+ longkey = "long:#{url}"
23
+ shortkey = "short:#{short}"
24
+ limitkey = "#{shortkey}:limit"
25
+
26
+ $redis.multi do
27
+ $redis.set limitkey, limit if limit
28
+ $redis.set longkey, short
29
+ $redis.set shortkey, url
30
+ end
31
+
32
+ if life
33
+ $redis.expire limitkey, limit if limit
34
+ $redis.expire longkey, life
35
+ $redis.expire shortkey, life
36
+ end
37
+ end
38
+
39
+ def shorturl short
40
+ request.path_info = "/#{short}"
41
+ request.url
42
+ end
43
+ end
44
+
45
+ get "/" do
46
+ IO.read "#{settings.public}/index.html"
47
+ end
48
+
49
+ get "/stats" do
50
+ @count = $redis.get(:counter).to_i
51
+ @hits = $redis.get(:hits).to_i
52
+ @misses = $redis.get(:misses).to_i
53
+ @byhits = Hash[*$redis.zrevrange("by:hits", 0, 10, :with_scores => true)]
54
+ @bytimes = Hash[*$redis.zrevrange("by:time", 0, 10, :with_scores => true)]
55
+
56
+ [@byhits, @bytimes].each do |h|
57
+ h.each { |k, v| h[k] = { long: $redis.get("short:#{k}"), score: v } }
58
+ end
59
+
60
+ @title = "Stats"
61
+ erb :stats
62
+ end
63
+
64
+ post "/" do
65
+ bad! "Missing url." unless url = params[:url]
66
+
67
+ url = "http://#{url}" unless /^http/i =~ url
68
+
69
+ bad! "Malformed url." unless (u = URI.parse url) && /^http/ =~ u.scheme
70
+
71
+ life = params[:life].to_i if params[:life]
72
+ limit = params[:limit].to_i if params[:limit]
73
+ short = params[:name] if params[:name] && /[-a-z0-9]+/i =~ params[:name]
74
+
75
+ bad! "Name is already taken." if short && $redis.exists("short:#{short}")
76
+
77
+ if !short && existing = $redis.get("long:#{url}")
78
+ halt 201, {}, shorturl(existing)
79
+ end
80
+
81
+ short ||= $redis.incr(:counter).to_sxg
82
+ save short, url, limit, life
83
+
84
+ [201, {}, shorturl(short)]
85
+ end
86
+
87
+ get "/:short" do |short|
88
+ long = $redis.get "short:#{short}"
89
+ $redis.incr(:misses) and nope! unless long
90
+
91
+ limited = $redis.exists("short:#{short}:limit") &&
92
+ $redis.decr("short:#{short}:limit") < 0
93
+
94
+ if limited
95
+ %W(long:#{long} short:#{short} short:#{short}:limit).each do |key|
96
+ $redis.del key
97
+ end
98
+
99
+ nope!
100
+ end
101
+
102
+ $redis.incr :hits
103
+ $redis.zincrby "by:hits", 1, short
104
+ $redis.zadd "by:time", Time.now.utc.to_i, short
105
+
106
+ redirect long
107
+ end
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html>
4
+ <head>
5
+ <title>Oso<%= " :: #@title" if @title %></title>
6
+ <link href="/style.css" rel="stylesheet" type="text/css">
7
+ </head>
8
+
9
+ <body>
10
+ <% if @title %>
11
+ <h1>
12
+ Oso :: <%= @title %>
13
+ <a class="btn" href="/">home</a>
14
+ </h1>
15
+ <% end %>
16
+
17
+ <%= yield %>
18
+
19
+ <script type="text/javascript"
20
+ src="http://code.jquery.com/jquery-1.5.min.js"></script>
21
+ </body>
22
+ </html>
@@ -0,0 +1,62 @@
1
+ <ul class="boxen">
2
+ <li>
3
+ <span class="num"><%= @count %></span>
4
+ urls
5
+ </li>
6
+
7
+ <li>
8
+ <span class="num"><%= @hits %></span>
9
+ hits
10
+ </li>
11
+
12
+ <li>
13
+ <span class="num"><%= @misses %></span>
14
+ misses
15
+ </li>
16
+ </ul>
17
+
18
+ <% if @hits + @misses > 0 %>
19
+ <h2>Recent Hits</h2>
20
+
21
+ <table cellspacing="0">
22
+ <thead>
23
+ <tr>
24
+ <th>Last Hit</th>
25
+ <th>URL</th>
26
+ <th>Shortened</th>
27
+ </tr>
28
+ </thead>
29
+
30
+ <tbody>
31
+ <% @bytimes.each do |short, data| %>
32
+ <tr>
33
+ <td><%= data[:score] %></td>
34
+ <td><a href="<%= data[:long] %>"><%= data[:long] %></a></td>
35
+ <td><a href="<%= shorturl(short) %>"><%= shorturl(short) %></a></td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+
41
+ <h2>Most Hits</h2>
42
+
43
+ <table cellspacing="0">
44
+ <thead>
45
+ <tr>
46
+ <th>Hits</th>
47
+ <th>URL</th>
48
+ <th>Shortened</th>
49
+ </tr>
50
+ </thead>
51
+
52
+ <tbody>
53
+ <% @byhits.each do |short, data| %>
54
+ <tr>
55
+ <td><%= data[:score] %></td>
56
+ <td><a href="<%= data[:long] %>"><%= data[:long] %></a></td>
57
+ <td><a href="<%= shorturl(short) %>"><%= shorturl(short) %></a></td>
58
+ </tr>
59
+ <% end %>
60
+ </tbody>
61
+ </table>
62
+ <% end %>
data/lib/oso.rb ADDED
@@ -0,0 +1,85 @@
1
+ require "timeout"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ # A simple client for the Oso URL shortener.
6
+
7
+ class Oso
8
+ include Timeout
9
+
10
+ # Raised for any error.
11
+
12
+ Error = Class.new StandardError
13
+
14
+ # Duh.
15
+
16
+ VERSION = "1.0.0"
17
+
18
+ # :nodoc:
19
+
20
+ def self.instance
21
+ @instance ||= new
22
+ end
23
+
24
+ # A static helper if you don't want to create new instances of the
25
+ # Oso class. Set the <tt>OSO_URL</tt> environment variable to
26
+ # control the server location. See +initialize+ for more
27
+ # information.
28
+
29
+ def self.shorten *args
30
+ instance.shorten(*args)
31
+ end
32
+
33
+ # A URI. Where does the Oso server live? If unspecified during
34
+ # initialization it'll default to the contents of the
35
+ # <tt>OSO_URL</tt> environment variable. If that's unset, it's
36
+ # <tt>"http://localhost:9292"</tt>.
37
+
38
+ attr_reader :url
39
+
40
+ # Create a new instance, optionally specifying the server +url+. See
41
+ # the +url+ property for defaults and fallbacks.
42
+
43
+ def initialize url = nil
44
+ url = url || ENV["OSO_URL"] || "http://localhost:9292"
45
+ url = "http://#{url}" unless /^http/ =~ url
46
+
47
+ @url = URI.parse url
48
+ @url.path = "/" if @url.path.empty?
49
+ end
50
+
51
+ # Create a short URL for +url+. Pass a <tt>:life</tt> option
52
+ # (integer, in seconds) to control how long the short URL will be
53
+ # active. Pass a <tt>:limit</tt> option to control how many times
54
+ # the URL can be hit before it's deactivated. Pass a <tt>:name</tt>
55
+ # option to explicitly set the shortened URL.
56
+ #
57
+ # +shorten+ will raise an Oso::Error if network or server
58
+ # conditions keep +url+ from being shortened in one second.
59
+
60
+ def shorten url, options = {}
61
+ params = options.merge url: url
62
+
63
+ params[:life] &&= params[:life].to_i
64
+ params[:limit] &&= params[:limit].to_i
65
+
66
+ timeout 1, Oso::Error do
67
+ begin
68
+ res = Net::HTTP.post_form @url, params
69
+ rescue Errno::ECONNREFUSED
70
+ raise Oso::Error, "Connection to #@url refused."
71
+ rescue Timeout::Error
72
+ raise Oso::Error, "Connection to #@url timed out."
73
+ rescue Errno::EINVAL, Errno::ECONNRESET, EOFError => e
74
+ raise Oso::Error, "Connection to #@url failed. (#{e.class.name})"
75
+ rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
76
+ raise Oso::Error, "#@url provided a bad response. (#{e.class.name})"
77
+ end
78
+
79
+ case res.code.to_i
80
+ when 201 then return res.body
81
+ else raise Oso::Error, "Unsuccessful shorten #{res.code}: #{res.body}"
82
+ end
83
+ end
84
+ end
85
+ end
data/test/test_oso.rb ADDED
@@ -0,0 +1,54 @@
1
+ require "minitest/autorun"
2
+
3
+ require "fakeweb"
4
+ require "mocha"
5
+ require "oso"
6
+
7
+ FakeWeb.allow_net_connect = false
8
+
9
+ class TestOso < MiniTest::Unit::TestCase
10
+ def setup
11
+ FakeWeb.clean_registry
12
+ ENV["OSO_URL"] = nil
13
+ end
14
+
15
+ def test_initialize
16
+ assert_equal URI.parse("http://localhost:9292/"), Oso.new.url
17
+
18
+ ENV["OSO_URL"] = "http://foo/"
19
+
20
+ assert_equal URI.parse("http://foo/"), Oso.new.url
21
+ assert_equal URI.parse("http://foo/"), Oso.new("http://foo/").url
22
+ assert_equal URI.parse("http://foo/"), Oso.new("http://foo").url
23
+ assert_equal URI.parse("http://foo/"), Oso.new("foo").url
24
+ end
25
+
26
+ def test_self_instance
27
+ assert_same Oso.instance, Oso.instance
28
+ end
29
+
30
+ def test_self_shorten
31
+ Oso.instance.expects(:shorten).with("foo").returns "bar"
32
+ assert_equal "bar", Oso.shorten("foo")
33
+ end
34
+
35
+ def test_shorten
36
+ FakeWeb.register_uri :post, "http://example.org/",
37
+ body: "http://example.org/1", status: 201
38
+
39
+ oso = Oso.new "example.org"
40
+
41
+ assert_equal "http://example.org/1", oso.shorten("whatever")
42
+ end
43
+
44
+ def test_shorten_bad
45
+ FakeWeb.register_uri :post, "http://example.org/",
46
+ body: "No luck.", status: 404
47
+
48
+ oso = Oso.new "example.org"
49
+
50
+ assert_raises Oso::Error do
51
+ oso.shorten "blah"
52
+ end
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oso
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 1.0.0
6
+ platform: ruby
7
+ authors:
8
+ - Audiosocket
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-02-17 00:00:00 -06:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: fakeweb
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - "="
23
+ - !ruby/object:Gem::Version
24
+ version: 1.3.0
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - "="
34
+ - !ruby/object:Gem::Version
35
+ version: 2.0.2
36
+ type: :development
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: mocha
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - "="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.9.12
47
+ type: :development
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: hoe
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 2.9.1
58
+ type: :development
59
+ version_requirements: *id004
60
+ description: |-
61
+ Audiosocket's minimal URL shortener. It's named "oso" because those
62
+ three letters look a bit like our logo.
63
+ email:
64
+ - it@audiosocket.com
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files:
70
+ - Manifest.txt
71
+ - lib/oso/public/robots.txt
72
+ - CHANGELOG.rdoc
73
+ - README.rdoc
74
+ files:
75
+ - .gems
76
+ - CHANGELOG.rdoc
77
+ - Manifest.txt
78
+ - README.rdoc
79
+ - Rakefile
80
+ - config.ru
81
+ - lib/oso.rb
82
+ - lib/oso/public/audiosocket.png
83
+ - lib/oso/public/favicon.ico
84
+ - lib/oso/public/index.css
85
+ - lib/oso/public/index.html
86
+ - lib/oso/public/robots.txt
87
+ - lib/oso/public/style.css
88
+ - lib/oso/server.rb
89
+ - lib/oso/views/layout.rhtml
90
+ - lib/oso/views/stats.rhtml
91
+ - test/test_oso.rb
92
+ - .gemtest
93
+ has_rdoc: true
94
+ homepage: http://github.com/audiosocket/oso
95
+ licenses: []
96
+
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --main
100
+ - README.rdoc
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: "0"
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: "0"
115
+ requirements: []
116
+
117
+ rubyforge_project: oso
118
+ rubygems_version: 1.5.2
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: Audiosocket's minimal URL shortener
122
+ test_files:
123
+ - test/test_oso.rb