toast 0.5.2 → 0.6.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.
@@ -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