windsor 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/LICENSE +13 -0
- data/README.rdoc +3 -0
- data/Rakefile +37 -0
- data/app/controllers/windsor_controller.rb +271 -0
- data/lib/tasks/windsor_tasks.rake +4 -0
- data/lib/windsor/rails.rb +9 -0
- data/lib/windsor/version.rb +3 -0
- data/lib/windsor.rb +4 -0
- metadata +88 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2011 ExactTarget, http://www.exacttarget.com/
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Windsor'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
Bundler::GemHelper.install_tasks
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
task :default => :test
|
@@ -0,0 +1,271 @@
|
|
1
|
+
class WindsorController < ApplicationController
|
2
|
+
rescue_from ActiveRecord::RecordNotFound, :with => :resource_not_found
|
3
|
+
rescue_from JSON::ParserError, :with => :unsupported_media_type
|
4
|
+
|
5
|
+
before_filter :get_resource, :except => [:index, :create]
|
6
|
+
before_filter :scope, :set_attributes, :check_implemented
|
7
|
+
|
8
|
+
attr_accessor :exceptional_attributes
|
9
|
+
attr_accessor :attributes_all_or_none
|
10
|
+
|
11
|
+
def index
|
12
|
+
@max_page_size = 100 unless @max_page_size
|
13
|
+
if params[:page].nil?
|
14
|
+
current_page_index = 0
|
15
|
+
offset = 0
|
16
|
+
else
|
17
|
+
current_page_index = params[:page].to_i - 1
|
18
|
+
offset = @max_page_size * current_page_index
|
19
|
+
end
|
20
|
+
items = model_class.where(scope).limit(@max_page_size).offset(offset).all
|
21
|
+
total_items = model_class.count
|
22
|
+
|
23
|
+
object = {
|
24
|
+
get_controller_name => items,
|
25
|
+
:pagination => get_pagination_object(total_items, current_page_index)
|
26
|
+
}
|
27
|
+
render_json object
|
28
|
+
end
|
29
|
+
|
30
|
+
def create
|
31
|
+
request_body = JSON.parse(request.body.read)
|
32
|
+
existing_attributes = enabled_attributes(hashity_hash(model_class.new)).map { |item| item.to_s }
|
33
|
+
request_body = prune_extra_attributes(request_body, existing_attributes)
|
34
|
+
|
35
|
+
model_object = model_class.new(request_body.merge(scope))
|
36
|
+
if model_object.save
|
37
|
+
headers["Location"] = get_self_link({"id" => model_object.id})
|
38
|
+
render_json model_object, :created
|
39
|
+
else
|
40
|
+
render_error 422, "InvalidResourceFields", "Some fields couldn't be validated.", model_object.errors.messages
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def show
|
45
|
+
render_json @model_object
|
46
|
+
end
|
47
|
+
|
48
|
+
def update
|
49
|
+
request_body = JSON.parse(request.body.read)
|
50
|
+
existing_attributes = enabled_attributes(hashity_hash(@model_object)).map { |item| item.to_s }
|
51
|
+
request_body = prune_extra_attributes(request_body, existing_attributes)
|
52
|
+
|
53
|
+
# Checks that there is an incoming attribute for every existing attribute (i.e. no partial updates)
|
54
|
+
missing_attributes = []
|
55
|
+
existing_attributes.each do |existing_attribute, existing_value|
|
56
|
+
unless request_body.include?(existing_attribute)
|
57
|
+
missing_attributes << existing_attribute
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
unless missing_attributes.empty?
|
62
|
+
errors = missing_attributes.map { |missing_attribute| { missing_attribute => "must be present" } }
|
63
|
+
render_error 422, "InvalidResourceFields", "Some fields couldn't be validated.", errors
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
# Compare the existing attributes and the passed-in attributes and remove any passed-in attributes with
|
68
|
+
# a value identical to the corresponding existing attribute. This is necessary because update_attributes
|
69
|
+
# will error out when trying to update an existing attribute with the same value, if that attribute has a
|
70
|
+
# uniqueness validation on it.
|
71
|
+
existing_attributes.each do |existing_attribute, existing_value|
|
72
|
+
if request_body[existing_attribute] == existing_value
|
73
|
+
request_body.delete existing_attribute
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
if @model_object.update_attributes(request_body)
|
78
|
+
render_json @model_object
|
79
|
+
else
|
80
|
+
render_error 422, "InvalidResourceFields", "Some fields couldn't be validated.", @model_object.errors.messages
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def destroy
|
85
|
+
@model_object.destroy
|
86
|
+
render :nothing => true, :status => 204
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_representation(object)
|
90
|
+
return object
|
91
|
+
end
|
92
|
+
|
93
|
+
# Called before every action, if you want to set the attributes of the representation
|
94
|
+
# redefine this function in your controller with a call to attributes and your paramaters.
|
95
|
+
# Example:
|
96
|
+
# def set_attributes
|
97
|
+
# attributes :all, :except => [:name]
|
98
|
+
# end
|
99
|
+
def set_attributes
|
100
|
+
attributes :all
|
101
|
+
end
|
102
|
+
|
103
|
+
def attributes(all_or_none, exceptional_attributes = { :except => [] })
|
104
|
+
@attributes_all_or_none = all_or_none
|
105
|
+
@exceptional_attributes = exceptional_attributes[:except].map! { |ex_attr| ex_attr.to_sym }
|
106
|
+
end
|
107
|
+
|
108
|
+
def enabled_attributes(object_hash)
|
109
|
+
@exceptional_attributes = Array(@exceptional_attributes)
|
110
|
+
raise ArgumentError unless [:all, :none].include?(@attributes_all_or_none)
|
111
|
+
|
112
|
+
if @attributes_all_or_none == :all
|
113
|
+
keys = object_hash.keys
|
114
|
+
#keys.map! { |key| key.to_sym }
|
115
|
+
keys - @exceptional_attributes.map { |enabled_attr| enabled_attr.to_s }
|
116
|
+
else
|
117
|
+
@exceptional_attributes.map { |enabled_attr| enabled_attr.to_s }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Called before every action, if you want to disable certain actions redefine
|
122
|
+
# this function in your controller with a call to actions and your paramaters.
|
123
|
+
# Example:
|
124
|
+
# def set_actions
|
125
|
+
# actions :all, :except => [:destroy]
|
126
|
+
# end
|
127
|
+
def set_actions
|
128
|
+
actions :all
|
129
|
+
end
|
130
|
+
|
131
|
+
def actions(all_or_none, exceptional_actions = { :except => [] })
|
132
|
+
@actions_all_or_none = all_or_none
|
133
|
+
@exceptional_actions = exceptional_actions[:except]
|
134
|
+
end
|
135
|
+
|
136
|
+
def enabled_actions
|
137
|
+
@actions_all_or_none ||= :all
|
138
|
+
@exceptional_actions ||= []
|
139
|
+
@exceptional_actions = Array(@exceptional_actions)
|
140
|
+
raise ArgumentError unless [:all, :none].include?(@actions_all_or_none)
|
141
|
+
|
142
|
+
if @actions_all_or_none == :all
|
143
|
+
[:index, :show, :create, :update, :destroy] - @exceptional_actions
|
144
|
+
else
|
145
|
+
@exceptional_actions
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def get_self_link(object_hash)
|
150
|
+
url_conditions = { :controller => get_controller_name, :action => "index" }
|
151
|
+
url_conditions.merge!(:action => "show", :id => object_hash["id"].to_s) unless object_hash["id"].nil?
|
152
|
+
url_for(url_conditions)
|
153
|
+
end
|
154
|
+
|
155
|
+
def get_controller_name
|
156
|
+
model_class.name.underscore.pluralize
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def get_pagination_object(total_items, current_page_index)
|
162
|
+
total_pages = (total_items.to_f / @max_page_size).ceil
|
163
|
+
total_pages = 1 if total_pages == 0 # Collections with no items in them still have 1 page.
|
164
|
+
last_page_index = total_pages - 1
|
165
|
+
query_parameters = request.query_parameters
|
166
|
+
pagination = {
|
167
|
+
:total_items => total_items,
|
168
|
+
:max_page_size => @max_page_size,
|
169
|
+
:first => page_link(1),
|
170
|
+
:last => page_link(total_pages.to_s)
|
171
|
+
}
|
172
|
+
unless current_page_index == last_page_index
|
173
|
+
pagination[:next] = page_link((current_page_index + 2))
|
174
|
+
end
|
175
|
+
if current_page_index >= 1
|
176
|
+
pagination[:previous] = page_link(current_page_index)
|
177
|
+
end
|
178
|
+
return pagination
|
179
|
+
end
|
180
|
+
|
181
|
+
def page_link(page_number)
|
182
|
+
query_parameters = request.query_parameters
|
183
|
+
base_url = request.base_url + request.path
|
184
|
+
query_parameters[:page] = page_number
|
185
|
+
return base_url + "?" + query_parameters.to_query
|
186
|
+
end
|
187
|
+
|
188
|
+
# Removes extra attributes passed in. Extra attributes is defined as attributes not sent in a GET.
|
189
|
+
def prune_extra_attributes(request_body, existing_attributes)
|
190
|
+
request_body.each do |request_attribute, request_value|
|
191
|
+
request_body.delete request_attribute unless existing_attributes.include?(request_attribute)
|
192
|
+
end
|
193
|
+
return request_body
|
194
|
+
end
|
195
|
+
|
196
|
+
def model_class
|
197
|
+
Kernel.const_get(self.controller_name.singularize.camelize )
|
198
|
+
end
|
199
|
+
|
200
|
+
def get_resource
|
201
|
+
@model_object = model_class.where(scope).find(params[:id])
|
202
|
+
end
|
203
|
+
|
204
|
+
def check_implemented
|
205
|
+
set_actions
|
206
|
+
unless enabled_actions.include?(params[:action].to_sym)
|
207
|
+
render_error(405, "MethodNotImplemented", "Method not implemented.", request.method.to_s.upcase)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def scope
|
212
|
+
{}
|
213
|
+
end
|
214
|
+
|
215
|
+
def prepare_representation(object)
|
216
|
+
object.merge!(:self => get_self_link(object))
|
217
|
+
attributes = enabled_attributes(object)
|
218
|
+
object.each do |key, value|
|
219
|
+
object.delete key unless attributes.include?(key)
|
220
|
+
end
|
221
|
+
to_representation(object)
|
222
|
+
end
|
223
|
+
|
224
|
+
def hashity_hash(item)
|
225
|
+
begin
|
226
|
+
return item.attributes
|
227
|
+
rescue NoMethodError
|
228
|
+
begin
|
229
|
+
return item.to_hash
|
230
|
+
rescue NoMethodError
|
231
|
+
raise ArgumentError, "Your object could not be converted to a hash."
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def render_json(object, status = 200)
|
237
|
+
begin
|
238
|
+
list_name = get_controller_name
|
239
|
+
if !object[list_name].nil? && object[list_name].is_a?(Array)
|
240
|
+
object[list_name].map! do |item|
|
241
|
+
prepare_representation(hashity_hash(item))
|
242
|
+
end
|
243
|
+
object[:self] = get_self_link(object)
|
244
|
+
else
|
245
|
+
object = prepare_representation(hashity_hash(object))
|
246
|
+
end
|
247
|
+
render :json => object, :status => status
|
248
|
+
rescue ArgumentError
|
249
|
+
if Rails.env.production? || Rails.env.test?
|
250
|
+
render_error(500, 'CouldNotRenderJSON', 'Could not render JSON')
|
251
|
+
else
|
252
|
+
render_error(500, 'CouldNotRenderJSON', 'Could not render JSON', object.inspect)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def render_error(status, type, message = "", detail = {})
|
258
|
+
raise ArgumentError unless status && type
|
259
|
+
raise ArgumentError unless message.is_a?(String)
|
260
|
+
render :json => { :error => { :type => type.to_s, :message => message, :detail => detail } }, :status => status
|
261
|
+
end
|
262
|
+
|
263
|
+
|
264
|
+
def resource_not_found
|
265
|
+
render_error(404, "ResourceNotFound", "Resource not found.")
|
266
|
+
end
|
267
|
+
|
268
|
+
def unsupported_media_type
|
269
|
+
render_error(415, "InvalidJSON", "Invalid JSON.", request.body.read)
|
270
|
+
end
|
271
|
+
end
|
data/lib/windsor.rb
ADDED
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: windsor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sam DeCesare
|
9
|
+
- Gregg Caines
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-09-27 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
requirement: &2156447380 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.1.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2156447380
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sqlite3
|
28
|
+
requirement: &2156446960 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *2156446960
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rspec
|
39
|
+
requirement: &2156446500 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *2156446500
|
48
|
+
description: Windsor is a plugin for building RESTful APIs in Rails.
|
49
|
+
email:
|
50
|
+
- sam@samdecesare.com
|
51
|
+
- gregg@caines.ca
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- app/controllers/windsor_controller.rb
|
57
|
+
- lib/tasks/windsor_tasks.rake
|
58
|
+
- lib/windsor/rails.rb
|
59
|
+
- lib/windsor/version.rb
|
60
|
+
- lib/windsor.rb
|
61
|
+
- LICENSE
|
62
|
+
- Rakefile
|
63
|
+
- README.rdoc
|
64
|
+
homepage: http://windsorapi.org
|
65
|
+
licenses: []
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.8.10
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: Windsor is a plugin for building RESTful APIs in Rails.
|
88
|
+
test_files: []
|