toast 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+ Summary
2
+ =======
3
+
4
+ Toast is an extension to Ruby on Rails that lets you expose any
5
+ ActiveRecord model as a resource according to the REST paradigm. The
6
+ representation format is JSON.
7
+
8
+ In contrast to other plugins, gems and Rails' inbuilt REST features
9
+ toast takes a data centric approach: Tell the model to be a resource
10
+ and what attributes and associations are to be exposed. That's it. No
11
+ controller boiler plate code for every model, no routing setup.
12
+
13
+ Toast is a Rails engine that runs one generic controller and a sets up
14
+ the routing to it according to the definition in the models, which is
15
+ denoted using a block oriented DSL.
16
+
17
+ REST is more than some pretty URIs, the use of the HTTP verbs and
18
+ response codes. It's on the toast user to invent media types that
19
+ control the application's state and introduce semantics. With toast you
20
+ can build REST services or tightly coupled server-client applications,
21
+ which ever suits the task best. That's why TOAST stands for:
22
+
23
+ > **TOast Ain't reST**
24
+
25
+ *Be careful*: This version is experimental and probably not bullet
26
+ proof. As soon as the gem is installed a controller with ready routing
27
+ is enabled serving the annotated model's data records for reading,
28
+ updating and deleting. There are no measures to prevent XSS and CSFR
29
+ attacks.
30
+
31
+ Example
32
+ =======
33
+
34
+ Let the table `bananas` have the following schema:
35
+
36
+ create_table "bananas", :force => true do |t|
37
+ t.string "name"
38
+ t.integer "number"
39
+ t.string "color"
40
+ t.integer "apple_id"
41
+ end
42
+
43
+ and let a corresponding model class have a *resourceful_model* annotation:
44
+
45
+ class Banana < ActiveRecord::Base
46
+ belongs_to :apple
47
+ has_many :coconuts
48
+ scope :find_some, where("number < 100")
49
+
50
+ resourceful_model do
51
+ # attributes or association names
52
+ fields :name, :number, :coconuts, :apple
53
+
54
+ # class methods of Banana returning an Array of Banana records
55
+ collections :find_some, :all
56
+ end
57
+ end
58
+
59
+ The above definition inside the `resourceful_model` block exposes the
60
+ records of the model Banana automatically via a generic controller to
61
+ the outside world, accepting and delivering JSON representations of
62
+ the records. Let the associated models Apple and Coconut be
63
+ exposed as a resource, too.
64
+
65
+ ### Get a collection
66
+ GET /bananas
67
+ --> 200, '[{"uri":"http://www.example.com/bananas/23",
68
+ "name": "Fred",
69
+ "number": 33,
70
+ "coconuts": "http://www.example.com/bananas/23/coconuts",
71
+ "apple":"http://www.example.com/bananas/23/apple,
72
+ {"uri":"http://www.example.com/bananas/24",
73
+ ... }, ... ]
74
+ ### Get a customized collection (filtered, paging, etc.)
75
+
76
+ GET /bananas/find_some
77
+ --> 200, '[SOME BANANAS]'
78
+
79
+ ### Get a single resource representation:
80
+ GET /bananas/23
81
+ --> 200, '{"uri":"http://www.example.com/bananas/23"
82
+ "name": "Fred",
83
+ "number": 33,
84
+ "coconuts": "http://www.example.com/bananas/23/coconuts",
85
+ "apple": "http://www.example.com/bananas/23/apple" }'
86
+
87
+ ### Get a associated collection
88
+ "GET" /bananas/23/coconuts
89
+ --> 200, '[{COCNUT},{COCONUT},...]',
90
+
91
+ ### Update a single resource:
92
+ PUT /bananas/23, '{"uri":"http://www.example.com/bananas/23"
93
+ "name": "Barney",
94
+ "number": 44}'
95
+ --> 200, '{"uri":"http://www.example.com/bananas/23"
96
+ "name": "Barney",
97
+ "number": 44,
98
+ "coconuts": "http://www.example.com/bananas/23/coconuts",
99
+ "apple": "http://www.example.com/bananas/23/apple"}'
100
+
101
+ ### Create a new record
102
+ "POST" /bananas, '{"name": "Johnny",
103
+ "number": 888}'
104
+ --> 201, {"uri":"http://www.example.com/bananas/102"
105
+ "name": "Johnny",
106
+ "number": 888,
107
+ "coconuts": "http://www.example.com/bananas/102/coconuts" ,
108
+ "apple": "http://www.example.com/bananas/102/apple }
109
+
110
+ ### Create an associated record
111
+ "POST" /bananas/23/coconuts, '{COCONUT}'
112
+ --> 201, {"uri":"http://www.example.com/coconuts/432,
113
+ ...}
114
+
115
+ ### Delete records
116
+ DELETE /bananas/23
117
+ --> 200
118
+
119
+ More details and configuration options are documented in the manual... (_comming soon_)
120
+
121
+ Installation
122
+ ============
123
+
124
+ git clone git@github.com:robokopp/toast.git
125
+ gem install jeweler
126
+ cd toast
127
+ rake install
128
+
129
+ Test Suite
130
+ ==========
131
+
132
+ In `test/rails_app` you can find a rails application with tests. To run
133
+ the tests you need to
134
+
135
+ 0. Install the *jeweler* gem:
136
+
137
+ gem install jeweler
138
+
139
+ 1. install the toast gem from this git clone:
140
+
141
+ rake install
142
+
143
+ 2. initialize the test application
144
+
145
+ cd test/rails_app
146
+ bundle install
147
+
148
+ 3. Now you can run the test suite from within the test application
149
+
150
+ rake
151
+
152
+ Or you may call `rake test` from the root directory of the working
153
+ copy. This will reinstall the toast gem before running tests
154
+ automatically.
@@ -0,0 +1,47 @@
1
+ class ToastController < ApplicationController
2
+
3
+ def catch_all
4
+
5
+ begin
6
+ resource = Toast::Resource.build( params, request )
7
+ render resource.apply(request.method, request.body.read)
8
+
9
+ rescue Toast::ResourceNotFound => e
10
+ return head(:not_found)
11
+
12
+ rescue Toast::PayloadInvalid => e
13
+ return head(:unprocessable_entity)
14
+
15
+ rescue Toast::PayloadFormatError => e
16
+ return head(:bad_request)
17
+
18
+ rescue Toast::MethodNotAllowed => e
19
+ return head(:method_not_allowed)
20
+
21
+ rescue Toast::UnsupportedMediaType => e
22
+ return head(:unsupported_media_type)
23
+
24
+ rescue Exception => e
25
+ log_exception e
26
+ raise e if Rails.env == "test"
27
+ return head(:internal_server_error)
28
+ end
29
+
30
+ end
31
+
32
+ def not_found
33
+ return head(:not_found)
34
+ end
35
+
36
+
37
+ if Rails.env == "test"
38
+ def log_exception e
39
+ puts "#{e.class}: '#{e.message}'\n\n#{e.backtrace[0..14].join("\n")}\n\n"
40
+ end
41
+ else
42
+ def log_exception e
43
+ logger.error("#{e.class}: '#{e.message}'\n\n#{e.backtrace.join("\n")}")
44
+ end
45
+ end
46
+
47
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,17 @@
1
+ Rails.application.routes.draw do
2
+
3
+ ActiveRecord::Base.descendants.each do |model|
4
+ next unless model.is_resourceful_model?
5
+
6
+ resource_name = model.to_s.pluralize.underscore
7
+
8
+ match "#{model.toast_config.namespace}/#{resource_name}(/:id(/:subresource))" => 'toast#catch_all', :constraints => { :id => /\d+/ }, :resource => resource_name
9
+ match "#{model.toast_config.namespace}/#{resource_name}/:subresource" => 'toast#catch_all', :resource => resource_name
10
+ end
11
+
12
+ match ":resource(/:id(/:subresource))" => 'toast#not_found', :constraints => { :id => /\d+/ }
13
+ match ":resource/:subresource" => 'toast#not_found'
14
+
15
+ end
16
+
17
+
data/lib/toast.rb ADDED
@@ -0,0 +1 @@
1
+ require 'toast/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
@@ -0,0 +1,76 @@
1
+ require 'blockenspiel'
2
+ require 'toast/config_dsl'
3
+
4
+ module Toast
5
+ module ActiveRecordExtensions
6
+
7
+ # Configuration DSL
8
+ def resourceful_model &block
9
+ @toast_config = Toast::ConfigDSL::Base.new(self)
10
+ Blockenspiel.invoke( block, @toast_config)
11
+
12
+ # add class methods
13
+ self.instance_eval do
14
+
15
+ cattr_accessor :uri_base
16
+
17
+ def is_resourceful_model?
18
+ true
19
+ end
20
+
21
+ def toast_config
22
+ @toast_config
23
+ end
24
+ end
25
+
26
+ # add instance methods
27
+ self.class_eval do
28
+ # Return toast's standard uri for a record
29
+ def uri
30
+ "#{self.class.uri_base}/#{self.class.to_s.pluralize.underscore}/#{self.id}"
31
+ end
32
+
33
+ # Returns a Hash with all exposed attributes
34
+ def exposed_attributes options = {}
35
+ options.reverse_merge! :in_collection => false,
36
+ :with_uri => true
37
+
38
+ # attributes
39
+ exposed_attr =
40
+ options[:in_collection] ? self.class.toast_config.in_collection.exposed_attributes :
41
+ self.class.toast_config.exposed_attributes
42
+
43
+ out = exposed_attr.inject({}) do |acc, attr|
44
+ acc[attr] = self[attr]
45
+ acc
46
+ end
47
+
48
+ # association URIs
49
+ exposed_assoc =
50
+ options[:in_collection] ? self.class.toast_config.in_collection.exposed_associations :
51
+ self.class.toast_config.exposed_associations
52
+
53
+ exposed_assoc.each do |assoc|
54
+ out[assoc] = self.uri + "/" + assoc
55
+ end
56
+
57
+ out["uri"] = self.uri if options[:with_uri]
58
+
59
+ out
60
+ end
61
+ end
62
+ end
63
+
64
+ # defaults for non resourceful-models
65
+ def is_resourceful_model?
66
+ false
67
+ end
68
+ def resourceful_model_options
69
+ nil
70
+ end
71
+ def resource_names
72
+ @@all_resourceful_resource_names
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1,58 @@
1
+ module Toast
2
+ class AssociateCollection < Resource
3
+
4
+ def initialize model, id, subresource_name
5
+ unless model.toast_config.exposed_associations.include? subresource_name
6
+ raise ResourceNotFound
7
+ end
8
+
9
+ @model = model
10
+ @record = model.find(id) rescue raise(ResourceNotFound)
11
+ @collection = subresource_name
12
+
13
+ @associate_model = Resource.get_class_by_resource_name subresource_name
14
+ @associate_model.uri_base = @model.uri_base
15
+
16
+ end
17
+
18
+ def get
19
+ records = @record.send(@collection)
20
+ {
21
+ :json => records.map{|r| r.exposed_attributes(:in_collection => true)},
22
+ :status => :ok
23
+ }
24
+ end
25
+
26
+ def put
27
+ raise MethodNotAllowed
28
+ end
29
+
30
+ def post payload
31
+
32
+
33
+ if self.media_type != @associate_model.toast_config.media_type
34
+ raise UnsupportedMediaType
35
+ end
36
+
37
+ if payload.keys.to_set != @associate_model.toast_config.exposed_attributes.to_set
38
+ raise PayloadInvalid
39
+ end
40
+
41
+ unless payload.is_a? Hash
42
+ raise PayloadFormatError
43
+ end
44
+ #debugger
45
+ record = @record.send(@collection).create payload
46
+
47
+ {
48
+ :json => record.exposed_attributes,
49
+ :location => record.uri,
50
+ :status => :created
51
+ }
52
+ end
53
+
54
+ def delete
55
+ raise MethodNotAllowed
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,38 @@
1
+ module Toast
2
+ class Attribute < Resource
3
+
4
+ def initialize model, id, attribute_name
5
+ unless model.toast_config.exposed_attributes.include? attribute_name
6
+ raise ResourceNotFound
7
+ end
8
+
9
+ @model = model
10
+ @record = model.find(id) rescue raise(ResourceNotFound)
11
+ @attribute_name = attribute_name
12
+ end
13
+
14
+ def get
15
+ {
16
+ :json => @record[@attribute_name],
17
+ :status => :ok
18
+ }
19
+ end
20
+
21
+ def put payload
22
+ @record.update_attribute(@attribute_name, payload)
23
+ {
24
+ :json => @record[@attribute_name],
25
+ :stauts => :ok,
26
+ :loaction => @record.uri
27
+ }
28
+ end
29
+
30
+ def post payload
31
+ raise MethodNotAllowed
32
+ end
33
+
34
+ def delete
35
+ raise MethodNotAllowed
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,112 @@
1
+ module Toast
2
+ module ConfigDSL
3
+
4
+ class Base
5
+ include Blockenspiel::DSL
6
+ dsl_attr_accessor :media_type, :has_many, :namespace
7
+
8
+ def initialize model
9
+ @model = model
10
+ @fields = []
11
+ @collections = []
12
+ @media_type = "application/json"
13
+ @exposed_attributes = []
14
+ @exposed_associations = []
15
+ @in_collection = ConfigDSL::InCollection.new model, self
16
+ end
17
+
18
+ def fields= *fields
19
+ @fields.push *ConfigDSL.sanitize(fields,"fields")
20
+ @fields.each do |attr_or_assoc|
21
+ if @model.new.attributes.keys.include? attr_or_assoc
22
+ @exposed_attributes << attr_or_assoc
23
+ else
24
+ @exposed_associations << attr_or_assoc
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ def fields *arg
31
+ return(@fields) if arg.empty?
32
+ self.fields = *arg
33
+ end
34
+
35
+ attr_reader :exposed_attributes, :exposed_associations
36
+
37
+ def collections= collections=[]
38
+ @collections = ConfigDSL.sanitize(collections, "collections")
39
+ end
40
+
41
+ def collections *arg
42
+ return(@collections) if arg.empty?
43
+ self.collections = *arg
44
+ end
45
+
46
+ def in_collection &block
47
+ if block_given?
48
+ Blockenspiel.invoke( block, @in_collection)
49
+ else
50
+ @in_collection
51
+ end
52
+ end
53
+
54
+ # non DSL methods
55
+ dsl_methods false
56
+ end
57
+
58
+ class InCollection
59
+ include Blockenspiel::DSL
60
+
61
+ def initialize model, base_config
62
+ @model = model
63
+ @fields = base_config.fields
64
+ @exposed_attributes = base_config.exposed_attributes
65
+ @exposed_associations = base_config.exposed_associations
66
+ @media_type = "application/json"
67
+ end
68
+
69
+ def fields= *fields
70
+ @fields = ConfigDSL.sanitize(fields,"fields")
71
+
72
+ @exposed_attributes = []
73
+ @exposed_associations = []
74
+
75
+ @fields.each do |attr_or_assoc|
76
+ if @model.new.attributes.keys.include? attr_or_assoc
77
+ @exposed_attributes << attr_or_assoc
78
+ else
79
+ @exposed_associations << attr_or_assoc
80
+ end
81
+ end
82
+ end
83
+
84
+ def fields *arg
85
+ return(@fields) if arg.empty?
86
+ self.fields = *arg
87
+ end
88
+
89
+ attr_reader :exposed_attributes, :exposed_associations
90
+ end
91
+
92
+
93
+ # Helper
94
+
95
+ # checks if list is made of symbols and strings
96
+ # converts a single value to an Array
97
+ # converts all symbols to strings
98
+ def self.sanitize list, name
99
+ list = [list].flatten
100
+
101
+ list.map do |x|
102
+ if (!x.is_a?(Symbol) && !x.is_a?(String))
103
+ raise "Toast Config Error: '#{name}' should be a list of Symbols or Strings"
104
+ else
105
+ x.to_s
106
+ end
107
+ end
108
+ end
109
+
110
+
111
+ end
112
+ end
@@ -0,0 +1,28 @@
1
+ require 'toast/active_record_extensions.rb'
2
+ require 'toast/resource.rb'
3
+ require 'toast/root_collection'
4
+ require 'toast/associate_collection'
5
+ require 'toast/attribute'
6
+ require 'toast/record'
7
+ require 'action_dispatch/http/request'
8
+
9
+ module Toast
10
+ class Engine < Rails::Engine
11
+
12
+ # configure our plugin on boot. other extension points such
13
+ # as configuration, rake tasks, etc, are also available
14
+ initializer "toast.initialize" do |app|
15
+ # Add 'restful_model' declaration to ActiveRecord::Base
16
+ ActiveRecord::Base.extend Toast::ActiveRecordExtensions
17
+
18
+ # Load all models in app/models early to setup routing
19
+ begin
20
+ Dir["#{Rails.root}/app/models/**/*.rb"].each{|m| require m }
21
+
22
+ rescue ActiveRecord::StatementInvalid
23
+ # raised when DB is not setup yet. (rake db:schema:load)
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ module Toast
2
+ class Record < Resource
3
+
4
+ def initialize model, id
5
+ @model = model
6
+ @record = model.find(id) rescue raise(ResourceNotFound.new)
7
+ end
8
+
9
+ def post payload
10
+ raise MethodNotAllowed
11
+ end
12
+
13
+ # get, put, delete, post return a Hash that can be used as
14
+ # argument for ActionController#render
15
+
16
+ def put payload
17
+ if self.media_type != @model.toast_config.media_type
18
+ raise UnsupportedMediaType
19
+ end
20
+
21
+ unless payload.is_a? Hash
22
+ raise PayloadFormatError
23
+ end
24
+
25
+ if payload.keys.to_set != @model.toast_config.exposed_attributes.to_set
26
+ raise PayloadInvalid
27
+ end
28
+
29
+ @record.update_attributes payload
30
+ {
31
+ :json => @record.exposed_attributes,
32
+ :status => :ok,
33
+ :location => @record.uri
34
+ }
35
+ end
36
+
37
+ def get
38
+ {
39
+ :json => @record.exposed_attributes,
40
+ :status => :ok
41
+ }
42
+ end
43
+
44
+ def delete
45
+ @record.destroy
46
+ {
47
+ :status => :ok
48
+ }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,86 @@
1
+ module Toast
2
+
3
+ class ResourceNotFound < Exception; end
4
+ class MethodNotAllowed < Exception; end
5
+ class PayloadInvalid < Exception; end
6
+ class PayloadFormatError < Exception; end
7
+ class UnsupportedMediaType < Exception; end
8
+
9
+ # Represents a resource. There are following resource types as sub classes:
10
+ # Record, RootCollection, AssociateCollection, Attribute
11
+ class Resource
12
+
13
+ attr_accessor :media_type
14
+
15
+ def initialize
16
+ raise 'ToastResource#new: use #build to create an instance'
17
+ end
18
+
19
+ def self.build params, request
20
+
21
+ resource_name = params[:resource]
22
+ id = params[:id]
23
+ subresource_name = params[:subresource]
24
+
25
+ uri_base = "#{request.protocol}#{request.host}"
26
+ unless (request.protocol == "http://" and request.port == 80) or
27
+ (request.protocol == "https://" and request.port == 443)
28
+ uri_base += ":#{request.port}"
29
+ end
30
+
31
+ begin
32
+
33
+ model = get_class_by_resource_name resource_name
34
+
35
+ model.uri_base = uri_base
36
+ model.uri_base += "/#{model.toast_config.namespace}" if model.toast_config.namespace
37
+
38
+ # decide which sub type
39
+ rsc = if id.nil?
40
+ Toast::RootCollection.new(model, subresource_name)
41
+ elsif subresource_name.nil?
42
+ Toast::Record.new(model, id)
43
+ elsif model.toast_config.exposed_associations.include? subresource_name
44
+ Toast::AssociateCollection.new(model, id, subresource_name)
45
+ elsif model.toast_config.exposed_attributes.include? subresource_name
46
+ Toast::Attribute.new(model, id, subresource_name)
47
+ else
48
+ raise ResourceNotFound
49
+ end
50
+
51
+ rsc.media_type = request.media_type
52
+
53
+ rsc
54
+ rescue NameError
55
+ raise ResourceNotFound
56
+ end
57
+ end
58
+
59
+ def self.get_class_by_resource_name name
60
+ begin
61
+ model = name.singularize.classify.constantize # raises NameError
62
+
63
+ unless ((model.superclass == ActiveRecord::Base) and model.is_resourceful_model?)
64
+ raise ResourceNotFound
65
+ end
66
+
67
+ model
68
+
69
+ rescue NameError
70
+ raise ResourceNotFound
71
+ end
72
+ end
73
+
74
+ def apply method, payload
75
+ case method
76
+ when "PUT","POST"
77
+ self.send(method.downcase, ActiveSupport::JSON.decode(payload))
78
+ when "DELETE","GET"
79
+ self.send(method.downcase)
80
+ else
81
+ raise MethodNotAllowed
82
+ end
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,58 @@
1
+ module Toast
2
+ class RootCollection < Resource
3
+
4
+ def initialize model, subresource_name
5
+
6
+ subresource_name ||= "all"
7
+
8
+ unless model.toast_config.collections.include? subresource_name
9
+ raise ResourceNotFound
10
+ end
11
+
12
+ @model = model
13
+ @collection = subresource_name
14
+ end
15
+
16
+ def get
17
+ records = @model.send(@collection)
18
+ {
19
+ :json => records.map{|r| r.exposed_attributes(:in_collection => true)},
20
+ :status => :ok
21
+ }
22
+ end
23
+
24
+ def put
25
+ raise MethodNotAllowed
26
+ end
27
+
28
+ def post payload
29
+ if @collection != "all"
30
+ raise MethodNotAllowed
31
+ end
32
+
33
+ if self.media_type != @model.toast_config.media_type
34
+ raise UnsupportedMediaType
35
+ end
36
+
37
+ unless payload.is_a? Hash
38
+ raise PayloadFormatError
39
+ end
40
+
41
+ if payload.keys.to_set != @model.toast_config.exposed_attributes.to_set
42
+ raise PayloadInvalid
43
+ end
44
+
45
+ record = @model.create payload
46
+
47
+ {
48
+ :json => record,
49
+ :location => record.uri,
50
+ :status => :created
51
+ }
52
+ end
53
+
54
+ def delete
55
+ raise MethodNotAllowed
56
+ end
57
+ end
58
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: toast
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Robert Annies
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-06-28 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: blockenspiel
16
+ requirement: &1015970 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.4.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *1015970
25
+ description: ! 'Toast is an extension to Ruby on Rails that lets you expose any
26
+
27
+ ActiveRecord model as a resource according to the REST paradigm. The
28
+
29
+ representation format is JSON.'
30
+ email: robokopp@fernwerk.net
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files:
34
+ - README.md
35
+ files:
36
+ - app/controller/toast_controller.rb
37
+ - config/routes.rb
38
+ - lib/toast.rb
39
+ - lib/toast/active_record_extensions.rb
40
+ - lib/toast/associate_collection.rb
41
+ - lib/toast/attribute.rb
42
+ - lib/toast/config_dsl.rb
43
+ - lib/toast/engine.rb
44
+ - lib/toast/record.rb
45
+ - lib/toast/resource.rb
46
+ - lib/toast/root_collection.rb
47
+ - README.md
48
+ homepage: https://github.com/robokopp/toast
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.5
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Toast adds a RESTful interface to ActiveRecord models in Ruby on Rails.
72
+ test_files: []