acceptable_api 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +172 -0
- data/Rakefile +2 -0
- data/TODO.md +13 -0
- data/acceptable_api.gemspec +21 -0
- data/example/.gitignore +2 -0
- data/example/Gemfile +10 -0
- data/example/config.ru +4 -0
- data/example/example.rb +41 -0
- data/lib/acceptable_api.rb +158 -0
- data/lib/acceptable_api/version.rb +3 -0
- metadata +103 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Craig R Webster
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# AcceptableApi
|
2
|
+
|
3
|
+
Build an acceptable API.
|
4
|
+
|
5
|
+
HTTP is pretty darn awesome you guys. Part of HTTP - the `Accept` header -
|
6
|
+
allows the clients of our API to tell us what representation they want to work
|
7
|
+
with. We should probably pay attention to them, hey?
|
8
|
+
|
9
|
+
This is expecially important when writing an API when you may need to deal with
|
10
|
+
several versions of a representation. When a client asks for JSON we don't
|
11
|
+
really know if they want JSON version 1 or 5 of our representation.
|
12
|
+
|
13
|
+
At some point I'll clean up my thoughts on this and write something decent.
|
14
|
+
Until then, more reading here:
|
15
|
+
|
16
|
+
http://barkingiguana.com/2011/12/05/principles-of-service-design-program-to-an-interface/
|
17
|
+
|
18
|
+
If you know better than me, please mail at me and tell me what I did wrong:
|
19
|
+
craig@barkingiguana.com.
|
20
|
+
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
gem 'acceptable_api'
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
$ gem install acceptable_api
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
Assume you have a class that you want to expose via a lovely HTTP API.
|
39
|
+
|
40
|
+
class Sandwich
|
41
|
+
def self.find id
|
42
|
+
# Look up a Sandwich by ID
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :fillings
|
46
|
+
attr_accessor :bread
|
47
|
+
attr_accessor :name
|
48
|
+
attr_accessor :made_at
|
49
|
+
end
|
50
|
+
|
51
|
+
You'd declare that you wanted to expose it via the API as `/sandwiches/123` like
|
52
|
+
this:
|
53
|
+
|
54
|
+
class SandwichApi < AcceptableApi::Controller
|
55
|
+
get '/sandwiches/:id' do
|
56
|
+
Sandwich.find params[:id]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Of course, this needs to be run somehow. An `AcceptableApi::Controller` can be
|
61
|
+
used as a Rack application. In `config.ru` do this:
|
62
|
+
|
63
|
+
require 'acceptable_api'
|
64
|
+
require 'sandwich_api'
|
65
|
+
|
66
|
+
run SandwichApi
|
67
|
+
|
68
|
+
You can now use `rackup` as normal to launch a web server, and `curl` to access
|
69
|
+
your API:
|
70
|
+
|
71
|
+
$ curl -i http://localhost:9292/sandwiches/123
|
72
|
+
HTTP/1.1 406 Not Acceptable
|
73
|
+
X-Frame-Options: sameorigin
|
74
|
+
X-Xss-Protection: 1; mode=block
|
75
|
+
Content-Type: application/javascript
|
76
|
+
Content-Length: 21
|
77
|
+
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
|
78
|
+
Date: Sat, 21 Apr 2012 19:57:59 GMT
|
79
|
+
Connection: Keep-Alive
|
80
|
+
|
81
|
+
{
|
82
|
+
"links": [
|
83
|
+
|
84
|
+
]
|
85
|
+
}
|
86
|
+
|
87
|
+
We got a `406 Not Acceptable` response because AcceptableApi doesn't know how to
|
88
|
+
respond with any of the representations we'd accept. That's fair: we haven't
|
89
|
+
told it how to respond to /any/ representations yet. Do it like this:
|
90
|
+
|
91
|
+
AcceptableApi.register Sandwich, 'application/json' do |sandwich, request|
|
92
|
+
JSON.generate :id => sandwich.id
|
93
|
+
end
|
94
|
+
|
95
|
+
Let's try requesting the resource again:
|
96
|
+
|
97
|
+
$ curl -i http://localhost:9293/sandwiches/123
|
98
|
+
HTTP/1.1 200 OK
|
99
|
+
X-Frame-Options: sameorigin
|
100
|
+
X-Xss-Protection: 1; mode=block
|
101
|
+
Content-Type: application/json
|
102
|
+
Content-Length: 12
|
103
|
+
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
|
104
|
+
Date: Sat, 21 Apr 2012 20:03:14 GMT
|
105
|
+
Connection: Keep-Alive
|
106
|
+
|
107
|
+
{"id":"123"}
|
108
|
+
|
109
|
+
Ace, we got a response. What happens if we ask for a plain text response?
|
110
|
+
|
111
|
+
$ curl -H 'Accept: text/plain' -i http://localhost:9292/sandwiches/123
|
112
|
+
HTTP/1.1 406 Not Acceptable
|
113
|
+
X-Frame-Options: sameorigin
|
114
|
+
X-Xss-Protection: 1; mode=block
|
115
|
+
Content-Type: application/javascript
|
116
|
+
Content-Length: 146
|
117
|
+
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
|
118
|
+
Date: Sat, 21 Apr 2012 20:04:46 GMT
|
119
|
+
Connection: Keep-Alive
|
120
|
+
|
121
|
+
{
|
122
|
+
"links": [
|
123
|
+
{
|
124
|
+
"rel": "alternative",
|
125
|
+
"type": "application/json",
|
126
|
+
"uri": "http://localhost:9293/sandwiches/123"
|
127
|
+
}
|
128
|
+
]
|
129
|
+
}
|
130
|
+
|
131
|
+
As expected, this is a `406 Not Acceptable` response, but we take the
|
132
|
+
opportunity to provide a list of alternative representations that the client may
|
133
|
+
want to check out. Our `application/json` is listed with the type and the URI to
|
134
|
+
request should the client want to do so.
|
135
|
+
|
136
|
+
Time passes, and a we decide that our API would be more useful if it returned
|
137
|
+
the fillings and bread used in the sandwich, and we want to replace the database
|
138
|
+
ID with the name of the sandwich. We want to continue supporting the old API
|
139
|
+
because lots of people are using it. We coin a new mime type in the
|
140
|
+
`application/vnd.*` space, something we really should have done to start with,
|
141
|
+
which specifies the returned document:
|
142
|
+
|
143
|
+
application/vnd.acme.sandwich-v1+json
|
144
|
+
|
145
|
+
A valid JSON document containing these keys and meanings:
|
146
|
+
|
147
|
+
name:: the name of the sandwich
|
148
|
+
fillings:: an array of fillings in the sandwich
|
149
|
+
bread:: the type of bread used in the sandwich
|
150
|
+
|
151
|
+
And we register the type with AcceptableApi:
|
152
|
+
|
153
|
+
AcceptableApi.register Sandwich, 'application/vnd.acme.sandwich-v1+json' do |sandwich, request|
|
154
|
+
JSON.generate :name => sandwich.name, :fillings => sandwich.fillings,
|
155
|
+
:bread => sandwich.bread
|
156
|
+
end
|
157
|
+
|
158
|
+
And we make the request:
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
See `example/example.rb` for an example.
|
163
|
+
|
164
|
+
TODO: Write usage instructions here
|
165
|
+
|
166
|
+
## Contributing
|
167
|
+
|
168
|
+
1. Fork it
|
169
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
170
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
171
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
172
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/TODO.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
* Work out if I need to use Sinatra or if I could strip out the routing
|
2
|
+
stuff - that's just about all I've used.
|
3
|
+
|
4
|
+
* Support other HTTP verbs. How could I create a Document from a POSTed
|
5
|
+
`application/vnd.acme.document-v1+xml`?
|
6
|
+
|
7
|
+
* Better documentation.
|
8
|
+
|
9
|
+
* A real test-driven codebase instead of the spike I've pulled together
|
10
|
+
here.
|
11
|
+
|
12
|
+
* Do we need to consider HATEOAS style resource links in AcceptableApi or is
|
13
|
+
that for a different project? Is anything special needed at all for this?
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/acceptable_api/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Craig R Webster"]
|
6
|
+
gem.email = ["craig@barkingiguana.com"]
|
7
|
+
gem.description = %q{HTTP lets clients sned an Accept header. We should probably use that to accept more than the bog-standard mime-types.}
|
8
|
+
gem.summary = %q{Build an Acceptable API}
|
9
|
+
gem.homepage = "http://barkingiguana.com/2011/12/05/principles-of-service-design-program-to-an-interface/"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "acceptable_api"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = AcceptableApi::VERSION
|
17
|
+
gem.add_runtime_dependency 'rack'
|
18
|
+
gem.add_runtime_dependency 'rack-accept'
|
19
|
+
gem.add_runtime_dependency 'rack-accept-header-updater'
|
20
|
+
gem.add_runtime_dependency 'sinatra'
|
21
|
+
end
|
data/example/.gitignore
ADDED
data/example/Gemfile
ADDED
data/example/config.ru
ADDED
data/example/example.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'json'
|
3
|
+
require 'builder'
|
4
|
+
|
5
|
+
# One of the resources we're working with.
|
6
|
+
# For simplicity I'm faking it out using OpenStruct.
|
7
|
+
class Sandwich < OpenStruct
|
8
|
+
# Look up a Sandwich by ID
|
9
|
+
def self.find id
|
10
|
+
new :fillings => %w(jam avacado anchovies), :bread => "brown",
|
11
|
+
:made_at => Time.now, :id => id, :name => "Bleaugh"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
AcceptableApi.register Sandwich, 'application/json' do |sandwich, request|
|
16
|
+
JSON.generate :id => sandwich.id
|
17
|
+
end
|
18
|
+
|
19
|
+
AcceptableApi.register Sandwich, 'application/vnd.acme.sandwich-v1+json' do |sandwich, request|
|
20
|
+
JSON.generate :name => sandwich.name, :fillings => sandwich.fillings,
|
21
|
+
:bread => sandwich.bread
|
22
|
+
end
|
23
|
+
|
24
|
+
AcceptableApi.register Sandwich, 'application/vnd.acme.sandwich-v1+xml' do |sandwich, request|
|
25
|
+
xml = Builder::XmlMarkup.new
|
26
|
+
xml.sandwich do |s|
|
27
|
+
s.name sandwich.name
|
28
|
+
s.bread sandwich.bread
|
29
|
+
s.fillings do |f|
|
30
|
+
sandwich.fillings.each do |filling|
|
31
|
+
f.filling filling
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Example < AcceptableApi::Controller
|
38
|
+
get '/sandwiches/:id' do
|
39
|
+
Sandwich.find params[:id]
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'singleton'
|
3
|
+
require 'rack/accept'
|
4
|
+
require 'rack/accept_header_updater'
|
5
|
+
|
6
|
+
require "acceptable_api/version"
|
7
|
+
|
8
|
+
module AcceptableApi
|
9
|
+
def self.register klass, mime_type, &map_block
|
10
|
+
Mapper.register klass, mime_type, &map_block
|
11
|
+
end
|
12
|
+
|
13
|
+
class MissingMapper
|
14
|
+
include Singleton
|
15
|
+
|
16
|
+
def mime_type
|
17
|
+
'application/javascript'
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute resource, request
|
21
|
+
# set the response to "no acceptable representation available"
|
22
|
+
mappers = Mapper.for resource.class
|
23
|
+
body = {
|
24
|
+
:links => mappers.mime_types.map do |mime_type|
|
25
|
+
{ :rel => "alternative", :type => mime_type, :uri => request.uri }
|
26
|
+
end
|
27
|
+
}
|
28
|
+
[ 406, { 'Content-Type' => 'application/javascript' }, JSON.pretty_generate(body) ]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Mappers
|
33
|
+
attr_accessor :mappers
|
34
|
+
private :mappers=, :mappers
|
35
|
+
|
36
|
+
def initialize mappers
|
37
|
+
self.mappers = mappers
|
38
|
+
end
|
39
|
+
|
40
|
+
def to accepts
|
41
|
+
acceptable_mime_types = accepts.order mime_types
|
42
|
+
acceptable_mappers = acceptable_mime_types.map { |mt|
|
43
|
+
mappers.detect { |m| m.mime_type == mt }
|
44
|
+
}
|
45
|
+
return acceptable_mappers if acceptable_mappers.any?
|
46
|
+
[ MissingMapper.instance ]
|
47
|
+
end
|
48
|
+
|
49
|
+
def mime_types
|
50
|
+
mappers.map { |m| m.mime_type }.sort.uniq
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Mapper
|
55
|
+
[ :klass, :mime_type, :map_block ].each do |a|
|
56
|
+
attr_accessor a
|
57
|
+
private "#{a}="
|
58
|
+
end
|
59
|
+
private :map_block
|
60
|
+
|
61
|
+
def initialize klass, mime_type, &map_block
|
62
|
+
self.klass = klass
|
63
|
+
self.mime_type = mime_type
|
64
|
+
self.map_block = map_block
|
65
|
+
end
|
66
|
+
|
67
|
+
def execute resource, request
|
68
|
+
body = map_block.call resource, request
|
69
|
+
[ status, headers, body ]
|
70
|
+
end
|
71
|
+
|
72
|
+
def status
|
73
|
+
200
|
74
|
+
end
|
75
|
+
|
76
|
+
def headers
|
77
|
+
{ 'Content-Type' => mime_type }
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.for klass
|
81
|
+
klass_mappers = mappers.select { |m| m.klass == klass }
|
82
|
+
Mappers.new klass_mappers
|
83
|
+
end
|
84
|
+
|
85
|
+
class << self;
|
86
|
+
attr_accessor :mappers
|
87
|
+
protected :mappers=, :mappers
|
88
|
+
end
|
89
|
+
self.mappers = []
|
90
|
+
|
91
|
+
def self.register klass, mime_type, &map_block
|
92
|
+
mapper = Mapper.new klass, mime_type, &map_block
|
93
|
+
self.mappers << mapper
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# This needs to mimic as much of the Sinatra API as we use.
|
98
|
+
# Probably we want to set headers and content_type, not sure what else.
|
99
|
+
class Response
|
100
|
+
attr_accessor :params
|
101
|
+
def initialize options = {}
|
102
|
+
self.params = options[:params]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Accepts
|
107
|
+
attr_accessor :request
|
108
|
+
private :request=, :request
|
109
|
+
|
110
|
+
def initialize env
|
111
|
+
self.request = Rack::Accept::Request.new env
|
112
|
+
end
|
113
|
+
|
114
|
+
def order mime_types
|
115
|
+
ordered = request.media_type.sort_with_qvalues mime_types, false
|
116
|
+
ordered.map! { |q, mt| mt }
|
117
|
+
ordered
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Request
|
122
|
+
attr_accessor :request
|
123
|
+
private :request=, :request
|
124
|
+
|
125
|
+
def initialize rack_request
|
126
|
+
self.request = rack_request
|
127
|
+
end
|
128
|
+
|
129
|
+
def respond_with resource
|
130
|
+
accepts = Accepts.new request.env
|
131
|
+
mappers = Mapper.for(resource.class).to(accepts)
|
132
|
+
mapper = mappers[0]
|
133
|
+
code, headers, body = mapper.execute resource, self
|
134
|
+
headers["Content-Length"] = body.bytesize.to_s
|
135
|
+
[ code, headers, body ]
|
136
|
+
end
|
137
|
+
|
138
|
+
def uri
|
139
|
+
request.url
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class Controller < Sinatra::Base
|
144
|
+
use Rack::AcceptHeaderUpdater
|
145
|
+
|
146
|
+
def self.get path, &block
|
147
|
+
super path do
|
148
|
+
response = Response.new :params => params
|
149
|
+
resource = response.instance_eval &block
|
150
|
+
api = Request.new request
|
151
|
+
s, h, body = api.respond_with resource
|
152
|
+
status s
|
153
|
+
headers h
|
154
|
+
body
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acceptable_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Craig R Webster
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-21 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: &70108579929200 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70108579929200
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rack-accept
|
27
|
+
requirement: &70108576320140 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70108576320140
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rack-accept-header-updater
|
38
|
+
requirement: &70108576318020 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70108576318020
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sinatra
|
49
|
+
requirement: &70108576317600 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70108576317600
|
58
|
+
description: HTTP lets clients sned an Accept header. We should probably use that
|
59
|
+
to accept more than the bog-standard mime-types.
|
60
|
+
email:
|
61
|
+
- craig@barkingiguana.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- .gitignore
|
67
|
+
- Gemfile
|
68
|
+
- LICENSE
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- TODO.md
|
72
|
+
- acceptable_api.gemspec
|
73
|
+
- example/.gitignore
|
74
|
+
- example/Gemfile
|
75
|
+
- example/config.ru
|
76
|
+
- example/example.rb
|
77
|
+
- lib/acceptable_api.rb
|
78
|
+
- lib/acceptable_api/version.rb
|
79
|
+
homepage: http://barkingiguana.com/2011/12/05/principles-of-service-design-program-to-an-interface/
|
80
|
+
licenses: []
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.8.10
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: Build an Acceptable API
|
103
|
+
test_files: []
|