oso 1.0.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/.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