toast 0.1.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/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: []