toast 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,7 +6,7 @@ class ToastController < ApplicationController
6
6
 
7
7
  @resource = Toast::Resource.build( params, request )
8
8
 
9
- render @resource.apply(request.method, request.body.read)
9
+ render @resource.apply(request.method, request.body.read, request.content_type)
10
10
 
11
11
  rescue Toast::ResourceNotFound => e
12
12
  return head(:not_found)
data/config/routes.rb CHANGED
@@ -5,17 +5,25 @@ Rails.application.routes.draw do
5
5
 
6
6
  resource_name = model.to_s.pluralize.underscore
7
7
 
8
- match("#{model.toast_config.namespace}/#{resource_name}(/:id(/:subresource))" => 'toast#catch_all',
9
- :constraints => { :id => /\d+/ },
10
- :resource => resource_name,
11
- :as => resource_name,
12
- :defaults => { :format => 'json' })
13
-
14
- match("#{model.toast_config.namespace}/#{resource_name}/:subresource" => 'toast#catch_all',
15
- :resource => resource_name,
16
- :defaults => { :format => 'json' })
17
- end
8
+ namespaces = []
18
9
 
19
- end
10
+ # routes must be defined for all defined namespaces of a model
11
+ model.toast_configs.each do |tc|
12
+ # once per namespace
13
+ next if namespaces.include? tc.namespace
14
+
15
+ namespaces << tc.namespace
16
+
17
+ match("#{tc.namespace}/#{resource_name}(/:id(/:subresource))" => 'toast#catch_all',
18
+ :constraints => { :id => /\d+/ },
19
+ :resource => resource_name,
20
+ :as => resource_name,
21
+ :defaults => { :format => 'json' })
20
22
 
23
+ match("#{tc.namespace}/#{resource_name}/:subresource" => 'toast#catch_all',
24
+ :resource => resource_name,
25
+ :defaults => { :format => 'json' })
26
+ end
27
+ end
21
28
 
29
+ end
@@ -4,57 +4,64 @@ require 'toast/config_dsl'
4
4
  module Toast
5
5
  module ActiveRecordExtensions
6
6
 
7
- # Configuration DSL
8
- def resourceful_model &block
9
- @toast_config = Toast::ConfigDSL::Base.new(self)
10
- Blockenspiel.invoke( block, @toast_config)
7
+ # Configuration DSL
8
+ def acts_as_resource &block
9
+
10
+ @toast_configs ||= Array.new
11
+
12
+ @toast_configs << Toast::ConfigDSL::Base.new(self)
13
+
14
+ Blockenspiel.invoke( block, @toast_configs.last)
11
15
 
12
16
  # add class methods
13
17
  self.instance_eval do
14
18
 
15
- cattr_accessor :uri_base
16
-
17
19
  def is_resourceful_model?
18
20
  true
19
21
  end
20
22
 
21
- def toast_config
22
- @toast_config
23
+ def toast_configs
24
+ @toast_configs
25
+ end
26
+
27
+ # get a config by media type or first one if none matches
28
+ def toast_config media_type
29
+ @toast_configs.find do |tc|
30
+ tc.media_type == media_type || tc.in_collection.media_type == media_type
31
+ end || @toast_configs.first
23
32
  end
24
33
  end
25
34
 
26
35
  # add instance methods
27
36
  self.class_eval do
28
37
  # Return the path segment of the URI of this record
29
- def uri_fullpath
38
+ def uri_path
30
39
  "/" +
31
- (self.class.toast_config.namespace ? self.class.toast_config.namespace+"/" : "") +
32
40
  self.class.to_s.pluralize.underscore + "/" +
33
41
  self.id.to_s
34
42
  end
43
+
44
+ # Like ActiveRecord::Base.attributes, but result Hash includes
45
+ # only attributes from the list _attr_names_ plus the
46
+ # associations _assoc_names_ as links and the 'self' link
47
+ def represent attr_names, assoc_names, base_uri
48
+ props = {}
35
49
 
36
- # Returns a Hash with all exposed attributes
37
- def exposed_attributes options = {}
38
- options.reverse_merge! :in_collection => false,
39
- :with_uri => true
40
-
41
- # attributes
42
- exposed_attr =
43
- options[:in_collection] ? self.class.toast_config.in_collection.exposed_attributes :
44
- self.class.toast_config.exposed_attributes
45
-
46
- out = exposed_attr.inject({}) do |acc, attr|
47
- acc[attr] = self.send attr
48
- acc
50
+ attr_names.each do |name|
51
+ props[name] = self.send(name)
49
52
  end
50
-
51
- out
53
+
54
+ assoc_names.each do |name|
55
+ props[name] = "#{base_uri}#{self.uri_path}/#{name}"
56
+ end
57
+
58
+ props["self"] = base_uri + self.uri_path
59
+
60
+ props
52
61
  end
53
62
  end
54
63
  end
55
64
 
56
- alias acts_as_resource resourceful_model
57
-
58
65
  # defaults for non resourceful-models
59
66
  def is_resourceful_model?
60
67
  false
@@ -3,8 +3,9 @@ module Toast
3
3
 
4
4
  attr_reader :model
5
5
 
6
- def initialize model, id, subresource_name, format
7
- unless model.toast_config.exposed_associations.include? subresource_name
6
+ def initialize model, id, subresource_name, format, config, assoc_model, assoc_config_in, assoc_config_out
7
+
8
+ unless config.exposed_associations.include? subresource_name
8
9
  raise ResourceNotFound
9
10
  end
10
11
 
@@ -13,28 +14,35 @@ module Toast
13
14
  @assoc = subresource_name
14
15
  @format = format
15
16
  @is_collection = [:has_many, :has_and_belongs_to_many].include? @model.reflect_on_association(@assoc.to_sym).macro
16
-
17
- @associate_model = Resource.get_class_by_resource_name subresource_name
18
- @associate_model.uri_base = @model.uri_base
17
+ @config = config
18
+ @associate_model = assoc_model
19
+ @associate_config_in = assoc_config_in
20
+ @associate_config_out = assoc_config_out
19
21
 
20
22
  end
21
23
 
22
24
  def get
23
25
  result = @record.send(@assoc)
24
26
 
27
+ raise ResourceNotFound if result.nil?
28
+
25
29
  if result.is_a? Array
26
30
  {
27
31
  :json => result.map{|r|
28
- r.exposed_attributes(:in_collection => true).
29
- merge( uri_fields(r, true) )
32
+ r.represent( @associate_config_out.in_collection.exposed_attributes,
33
+ @associate_config_out.in_collection.exposed_associations,
34
+ @base_uri )
30
35
  },
31
- :status => :ok
36
+ :status => :ok,
37
+ :content_type => @associate_config_out.in_collection.media_type
32
38
  }
33
39
  else
34
40
  {
35
- :json => result.exposed_attributes(:in_collection => true).
36
- merge( uri_fields(result) ),
37
- :status => :ok
41
+ :json => result.represent( @associate_config_out.exposed_attributes,
42
+ @associate_config_out.exposed_associations,
43
+ @base_uri ),
44
+ :status => :ok,
45
+ :content_type => @associate_config_out.media_type
38
46
  }
39
47
  end
40
48
 
@@ -44,10 +52,10 @@ module Toast
44
52
  raise MethodNotAllowed
45
53
  end
46
54
 
47
- def post payload
48
- raise MethodNotAllowed unless @model.toast_config.writables.include? @assoc
55
+ def post payload, media_type
56
+ raise MethodNotAllowed unless @config.writables.include? @assoc
49
57
 
50
- if self.media_type != @associate_model.toast_config.media_type
58
+ if media_type != @associate_config_in.media_type
51
59
  raise UnsupportedMediaType
52
60
  end
53
61
 
@@ -63,18 +71,20 @@ module Toast
63
71
 
64
72
 
65
73
  # silently ignore all exposed readable, but not writable fields
66
- (@associate_model.toast_config.readables - @associate_model.toast_config.writables).each do |rof|
74
+ (@associate_config_in.readables - @associate_config_in.writables).each do |rof|
67
75
  payload.delete(rof)
68
76
  end
69
77
 
70
-
71
78
  begin
72
79
  record = @record.send(@assoc).create! payload
73
80
 
74
81
  {
75
- :json => record.exposed_attributes.merge( uri_fields(record) ),
76
- :location => self.base_uri + record.uri_fullpath,
77
- :status => :created
82
+ :json => record.represent( @associate_config_out.exposed_attributes,
83
+ @associate_config_out.exposed_associations,
84
+ @base_uri ),
85
+ :location => self.base_uri + record.uri_path,
86
+ :status => :created,
87
+ :content_type => @associate_config_out.media_type
78
88
  }
79
89
 
80
90
  rescue ActiveRecord::RecordInvalid => e
@@ -3,11 +3,11 @@ module Toast
3
3
 
4
4
  attr_reader :model
5
5
 
6
- def initialize model, subresource_name, params
6
+ def initialize model, subresource_name, params, config_in, config_out
7
7
 
8
8
  subresource_name ||= "all"
9
9
 
10
- unless model.toast_config.collections.include? subresource_name
10
+ unless config_out.collections.include? subresource_name
11
11
  raise ResourceNotFound
12
12
  end
13
13
 
@@ -15,11 +15,13 @@ module Toast
15
15
  @collection = subresource_name
16
16
  @params = params
17
17
  @format = params[:format]
18
+ @config_out = config_out
19
+ @config_in = config_in
18
20
  end
19
21
 
20
22
  def get
21
23
 
22
- records = if @model.toast_config.pass_params_to.include?(@collection)
24
+ records = if @config_out.pass_params_to.include?(@collection)
23
25
  @model.send(@collection, @params)
24
26
  else
25
27
  @model.send(@collection)
@@ -34,10 +36,12 @@ module Toast
34
36
  when "json"
35
37
  {
36
38
  :json => records.map{|r|
37
- r.exposed_attributes(:in_collection => true).
38
- merge( uri_fields(r, true) )
39
+ r.represent( @config_out.in_collection.exposed_attributes,
40
+ @config_out.in_collection.exposed_associations,
41
+ @base_uri )
39
42
  },
40
- :status => :ok
43
+ :status => :ok,
44
+ :content_type => @config_out.in_collection.media_type
41
45
  }
42
46
  else
43
47
  raise ResourceNotFound
@@ -48,15 +52,15 @@ module Toast
48
52
  raise MethodNotAllowed
49
53
  end
50
54
 
51
- def post payload
52
- raise MethodNotAllowed unless @model.toast_config.postable?
55
+ def post payload, media_type
56
+ raise MethodNotAllowed unless @config_in.postable?
53
57
 
54
- if @collection != "all"
55
- raise MethodNotAllowed
58
+ if media_type != @config_in.media_type
59
+ raise UnsupportedMediaType
56
60
  end
57
61
 
58
- if self.media_type != @model.toast_config.media_type
59
- raise UnsupportedMediaType
62
+ if @collection != "all"
63
+ raise MethodNotAllowed
60
64
  end
61
65
 
62
66
  begin
@@ -69,7 +73,7 @@ module Toast
69
73
  end
70
74
 
71
75
  # silently ignore all exposed readable, but not writable fields
72
- (@model.toast_config.readables - @model.toast_config.writables + ["uri"]).each do |rof|
76
+ (@config_in.readables - @config_in.writables + ["self"]).each do |rof|
73
77
  payload.delete(rof)
74
78
  end
75
79
 
@@ -77,9 +81,12 @@ module Toast
77
81
  record = @model.create! payload
78
82
 
79
83
  {
80
- :json => record.exposed_attributes.merge( uri_fields(record) ),
81
- :location => self.base_uri + record.uri_fullpath,
82
- :status => :created
84
+ :json => record.represent( @config_out.exposed_attributes,
85
+ @config_out.exposed_associations,
86
+ @base_uri ),
87
+ :location => @base_uri + record.uri_path,
88
+ :status => :created,
89
+ :content_type => @config_out.media_type
83
90
  }
84
91
 
85
92
  rescue ActiveRecord::RecordInvalid => e
@@ -3,7 +3,7 @@ module Toast
3
3
 
4
4
  class Base
5
5
  include Blockenspiel::DSL
6
- dsl_attr_accessor :media_type, :has_many, :namespace
6
+ dsl_attr_accessor :namespace, :media_type
7
7
 
8
8
  def initialize model
9
9
  @model = model
@@ -11,11 +11,11 @@ module Toast
11
11
  @writables = []
12
12
  @collections = []
13
13
  @singles = []
14
- @media_type = "application/json"
15
14
  @deletable = false
16
15
  @postable = false
17
16
  @pass_params_to = []
18
17
  @in_collection = ConfigDSL::InCollection.new model, self
18
+ @media_type = "application/json"
19
19
  end
20
20
 
21
21
  def exposed_attributes
@@ -107,11 +107,13 @@ module Toast
107
107
  class InCollection
108
108
  include Blockenspiel::DSL
109
109
 
110
+ dsl_attr_accessor :media_type
111
+
110
112
  def initialize model, base_config
111
113
  @model = model
114
+ @media_type = "application/json"
112
115
  @readables = base_config.readables # must assign a reference
113
116
  @writables = base_config.writables # must assign a reference
114
- @media_type = "application/json"
115
117
  end
116
118
 
117
119
  def readables= readables
@@ -127,13 +129,23 @@ module Toast
127
129
  def writables *arg
128
130
  self.writables = 42
129
131
  end
130
-
132
+
131
133
  def writables= arg
132
134
  puts
133
135
  puts "Toast Config Warning (#{model.class}): Defining \"writables\" in collection definition has no effect."
134
136
  puts
135
137
  end
136
138
 
139
+ def namespace *arg
140
+ self.writables = 42
141
+ end
142
+
143
+ def namespace= arg
144
+ puts
145
+ puts "Toast Config Warning (#{model.class}): Defining \"namespace\" in collection definition has no effect."
146
+ puts
147
+ end
148
+
137
149
  def exposed_attributes
138
150
  assocs = @model.reflect_on_all_associations.map{|a| a.name.to_s}
139
151
  (@readables + @writables).uniq.select{|f| !assocs.include?(f)}
@@ -143,6 +155,7 @@ module Toast
143
155
  assocs = @model.reflect_on_all_associations.map{|a| a.name.to_s}
144
156
  (@readables + @writables).uniq.select{|f| assocs.include?(f)}
145
157
  end
158
+
146
159
  end
147
160
 
148
161
 
data/lib/toast/engine.rb CHANGED
@@ -6,6 +6,7 @@ require 'toast/record'
6
6
  require 'toast/single'
7
7
 
8
8
  require 'action_dispatch/http/request'
9
+ require 'rack/accept_media_types'
9
10
 
10
11
  module Toast
11
12
  class Engine < Rails::Engine
data/lib/toast/record.rb CHANGED
@@ -3,22 +3,24 @@ module Toast
3
3
 
4
4
  attr_reader :model
5
5
 
6
- def initialize model, id, format
6
+ def initialize model, id, format, config_in, config_out
7
7
  @model = model
8
8
  @record = model.find(id) rescue raise(ResourceNotFound.new)
9
9
  @format = format
10
+ @config_in = config_in
11
+ @config_out = config_out
10
12
  end
11
13
 
12
- def post payload
14
+ def post payload, media_type
13
15
  raise MethodNotAllowed
14
16
  end
15
17
 
16
18
  # get, put, delete, post return a Hash that can be used as
17
19
  # argument for ActionController#render
18
20
 
19
- def put payload
21
+ def put payload, media_type
20
22
 
21
- if self.media_type != @model.toast_config.media_type
23
+ if media_type != @config_in.media_type
22
24
  raise UnsupportedMediaType
23
25
  end
24
26
 
@@ -33,21 +35,24 @@ module Toast
33
35
  end
34
36
 
35
37
  # ignore all exposed readable, but not writable fields
36
- (@model.toast_config.readables - @model.toast_config.writables + ["uri"]).each do |rof|
38
+ (@config_in.readables - @config_in.writables + ["self"]).each do |rof|
37
39
  payload.delete(rof)
38
40
  end
39
-
41
+
40
42
  # set the virtual attributes
41
- (payload.keys.to_set - @record.attribute_names.to_set).each do |vattr|
43
+ (@config_in.writables - @record.attribute_names - @config_in.exposed_associations).each do |vattr|
42
44
  @record.send("#{vattr}=", payload.delete(vattr))
43
45
  end
44
-
46
+
45
47
  # mass-update for the rest
46
48
  @record.update_attributes payload
47
- {
48
- :json => @record.exposed_attributes.merge( uri_fields(@record) ),
49
+ {
50
+ :json => @record.represent( @config_out.exposed_attributes,
51
+ @config_out.exposed_associations,
52
+ @base_uri ),
49
53
  :status => :ok,
50
- :location => self.base_uri + @record.uri_fullpath
54
+ :location => self.base_uri + @record.uri_path,
55
+ :content_type => @config_out.media_type
51
56
  }
52
57
  end
53
58
 
@@ -60,8 +65,11 @@ module Toast
60
65
  }
61
66
  when "json"
62
67
  {
63
- :json => @record.exposed_attributes.merge( uri_fields(@record) ),
64
- :status => :ok
68
+ :json => @record.represent( @config_out.exposed_attributes,
69
+ @config_out.exposed_associations,
70
+ @base_uri ),
71
+ :status => :ok,
72
+ :content_type => @config_out.media_type
65
73
  }
66
74
  else
67
75
  raise ResourceNotFound
@@ -69,7 +77,7 @@ module Toast
69
77
  end
70
78
 
71
79
  def delete
72
- raise MethodNotAllowed unless @model.toast_config.deletable?
80
+ raise MethodNotAllowed unless @config_out.deletable?
73
81
 
74
82
  @record.destroy
75
83
  {
@@ -5,12 +5,13 @@ module Toast
5
5
  class PayloadInvalid < Exception; end
6
6
  class PayloadFormatError < Exception; end
7
7
  class UnsupportedMediaType < Exception; end
8
+ class RequestedVersionNotDefined < Exception; end
8
9
 
9
10
  # Represents a resource. There are following resource types as sub classes:
10
11
  # Record, Collection, Association, Single
11
12
  class Resource
12
13
 
13
- attr_accessor :media_type, :base_uri
14
+ attr_accessor :prefered_media_type, :base_uri, :payload_content_type
14
15
 
15
16
  def initialize
16
17
  raise 'ToastResource#new: use #build to create an instance'
@@ -22,25 +23,56 @@ module Toast
22
23
  subresource_name = params[:subresource]
23
24
  format = params[:format]
24
25
 
25
- begin
26
+ #### Debugging stop
27
+ # binding.pry if $halt
28
+ ###
26
29
 
30
+ begin
31
+
32
+ # determine model
27
33
  model = get_class_by_resource_name resource_name
28
-
29
- # decide which sub type
30
- rsc = if id.nil? and model.toast_config.singles.include?(subresource_name)
31
- Toast::Single.new(model, subresource_name, params.clone)
34
+
35
+ # determine config for representation
36
+ # config_in: cosumed representation
37
+ # config_out: produced representation
38
+ config_out = model.toast_config request.accept_media_types.prefered
39
+ config_in = model.toast_config request.media_type
40
+
41
+ # ... or in case of an association request
42
+ config_assoc_src = model.toast_config request.headers["Assoc-source-type"]
43
+
44
+ # base URI for returned object
45
+ base_uri = request.base_url + request.script_name +
46
+ (config_out.namespace ? "/" + config_out.namespace : "")
47
+
48
+ # decide which sub resource type
49
+ rsc = if id.nil? and config_out.singles.include?(subresource_name)
50
+ Toast::Single.new(model, subresource_name, params.clone, config_in, config_out)
32
51
  elsif id.nil?
33
- Toast::Collection.new(model, subresource_name, params.clone)
52
+ Toast::Collection.new(model, subresource_name, params.clone, config_in, config_out)
34
53
  elsif subresource_name.nil?
35
- Toast::Record.new(model, id, format)
36
- elsif model.toast_config.exposed_associations.include? subresource_name
37
- Toast::Association.new(model, id, subresource_name, format)
54
+ Toast::Record.new(model, id, format, config_in, config_out)
55
+ elsif (config_assoc_src && config_assoc_src.exposed_associations.include?(subresource_name))
56
+
57
+ # determine associated model
58
+ assoc_model = get_class_by_resource_name subresource_name
59
+
60
+ # determine config for representation of assoc. model
61
+ assoc_config_out = assoc_model.toast_config request.accept_media_types.prefered
62
+ assoc_config_in = assoc_model.toast_config request.media_type
63
+
64
+ # change base URI to associated record
65
+ base_uri = request.base_url + request.script_name +
66
+ (assoc_config_out.namespace ? "/" + assoc_config_out.namespace : "")
67
+
68
+ Toast::Association.new(model, id, subresource_name, format, config_assoc_src,
69
+ assoc_model, assoc_config_in, assoc_config_out)
38
70
  else
39
71
  raise ResourceNotFound
40
72
  end
41
73
 
42
- rsc.media_type = request.media_type
43
- rsc.base_uri = request.base_url + request.script_name
74
+ # set base to be prepended to URIs
75
+ rsc.base_uri = base_uri
44
76
 
45
77
  rsc
46
78
  rescue NameError
@@ -63,10 +95,10 @@ module Toast
63
95
  end
64
96
  end
65
97
 
66
- def apply method, payload
98
+ def apply method, payload, payload_media_type
67
99
  case method
68
- when "PUT","POST"
69
- self.send(method.downcase, payload)
100
+ when "PUT","POST"
101
+ self.send(method.downcase, payload, payload_media_type)
70
102
  when "DELETE","GET"
71
103
  self.send(method.downcase)
72
104
  else
@@ -83,10 +115,10 @@ module Toast
83
115
  record.class.toast_config.exposed_associations
84
116
 
85
117
  exposed_assoc.each do |assoc|
86
- out[assoc] = "#{self.base_uri}#{record.uri_fullpath}/#{assoc}"
118
+ out[assoc] = "#{self.base_uri}#{record.uri_path}/#{assoc}"
87
119
  end
88
120
 
89
- out["uri"] = self.base_uri + record.uri_fullpath
121
+ out["self"] = self.base_uri + record.uri_path
90
122
 
91
123
  out
92
124
  end
data/lib/toast/single.rb CHANGED
@@ -13,9 +13,11 @@ module Toast
13
13
 
14
14
  attr_reader :model
15
15
 
16
- def initialize model, subresource_name, params
16
+ def initialize model, subresource_name, params, config_in, config_out
17
+ @config_in = config_in
18
+ @config_out = config_out
17
19
 
18
- unless model.toast_config.singles.include? subresource_name
20
+ unless @config_out.singles.include? subresource_name
19
21
  raise ResourceNotFound
20
22
  end
21
23
 
@@ -23,14 +25,14 @@ module Toast
23
25
  @params = params
24
26
  @format = params[:format]
25
27
 
26
- @record = if @model.toast_config.pass_params_to.include?(subresource_name)
28
+
29
+ @record = if @config_out.pass_params_to.include?(subresource_name)
27
30
  @model.send(subresource_name, @params)
28
31
  else
29
32
  @model.send(subresource_name)
30
33
  end
31
-
32
- raise ResourceNotFound if @record.nil?
33
-
34
+
35
+ raise ResourceNotFound if @record.nil?
34
36
  end
35
37
 
36
38
  def get
@@ -42,7 +44,9 @@ module Toast
42
44
  }
43
45
  when "json"
44
46
  {
45
- :json => @record.exposed_attributes.merge( uri_fields(@record) ),
47
+ :json => @record.represent( @config_out.exposed_attributes,
48
+ @config_out.exposed_associations,
49
+ @base_uri ),
46
50
  :status => :ok
47
51
  }
48
52
  else
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 5
8
- - 2
9
- version: 0.5.2
7
+ - 6
8
+ - 0
9
+ version: 0.6.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Robert Annies
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2012-03-21 00:00:00 +01:00
17
+ date: 2012-05-31 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -31,6 +31,19 @@ dependencies:
31
31
  version: 0.4.2
32
32
  type: :runtime
33
33
  version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rack-accept-media-types
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ - 9
44
+ version: "0.9"
45
+ type: :runtime
46
+ version_requirements: *id002
34
47
  description: |-
35
48
  Toast is an extension to Ruby on Rails that lets you expose any
36
49
  ActiveRecord model as a resource according to the REST paradigm. The