serviceable 0.5 → 0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +32 -2
- data/lib/serviceable.rb +119 -10
- metadata +5 -5
data/README.md
CHANGED
@@ -13,10 +13,18 @@ Controller:
|
|
13
13
|
|
14
14
|
end
|
15
15
|
|
16
|
-
|
16
|
+
Basic Routes:
|
17
17
|
|
18
18
|
resources :posts
|
19
19
|
|
20
|
+
Advanced Feature Routes:
|
21
|
+
|
22
|
+
resources :posts do
|
23
|
+
collection do
|
24
|
+
get :count
|
25
|
+
get :describe
|
26
|
+
end
|
27
|
+
end
|
20
28
|
|
21
29
|
## Standard CRUD
|
22
30
|
|
@@ -26,6 +34,23 @@ Route:
|
|
26
34
|
PUT /posts/1.json
|
27
35
|
DELETE /posts/1.json
|
28
36
|
|
37
|
+
## Advanced Features
|
38
|
+
|
39
|
+
Retrieve the number of records using the given query params
|
40
|
+
|
41
|
+
GET /posts/count.json
|
42
|
+
86
|
43
|
+
|
44
|
+
Use query params to filter the set
|
45
|
+
|
46
|
+
GET /posts/count.json?where[posts][author_id]=123
|
47
|
+
14
|
48
|
+
|
49
|
+
Discover the available extensions beyond basic CRUD, such as allowed includes and methods
|
50
|
+
|
51
|
+
GET /posts/describe.json
|
52
|
+
{"allowed_includes":["tags","author"],"allowed_methods":[]}
|
53
|
+
|
29
54
|
## Query Params
|
30
55
|
|
31
56
|
Full listing returned when no query params are given
|
@@ -33,7 +58,7 @@ Full listing returned when no query params are given
|
|
33
58
|
GET /posts.json
|
34
59
|
[{"id":1,"title":"First Post!","body":"Feels good to be first","created_at":"20130727T16:26:00Z"}]
|
35
60
|
|
36
|
-
Use the <code>only</code>
|
61
|
+
Use the <code>only</code> and/or <code>except</code> params to specify fields on the collection
|
37
62
|
|
38
63
|
GET /posts.json?only=id,title
|
39
64
|
[{"id":1,"title","First post!"}]
|
@@ -43,6 +68,11 @@ Use the <code>include</code> param to specify associated objects or collections
|
|
43
68
|
GET /posts.json?include=user
|
44
69
|
[{"id":1,"title":"First Post!","body":"Feels good to be first","created_at":"20130727T16:26:00Z","user":{"id":2,"first_name":"Jim","last_name":"Walker","display_name":"Jim W."}}]
|
45
70
|
|
71
|
+
Use the <code>methods</code> param to include the return values from a set of methods on each object
|
72
|
+
|
73
|
+
GET /posts.json?methods=max_rating
|
74
|
+
[{"id":1,"title":"First Post!","body":"Feels good to be first","created_at":"20130727T16:26:00Z","max_rating":3}]
|
75
|
+
|
46
76
|
Combine params to configure the result contents
|
47
77
|
|
48
78
|
GET /posts.json?only=id,title&include[user][only]=first_name,last_name
|
data/lib/serviceable.rb
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
class Hash
|
2
|
+
def &(other)
|
3
|
+
reject {|k,v| !(other.include?(k) && ([v]&[other[k]]).any?)}
|
4
|
+
end
|
5
|
+
def compact
|
6
|
+
reject {|k,v| k.nil? || v.nil?}
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
1
10
|
module Serviceable
|
2
11
|
|
3
12
|
def self.included(base)
|
@@ -13,17 +22,17 @@ module Serviceable
|
|
13
22
|
# acts_as_service :post
|
14
23
|
# end
|
15
24
|
#
|
16
|
-
def acts_as_service(object,
|
25
|
+
def acts_as_service(object,defaults={})
|
17
26
|
|
18
27
|
before_filter :assign_new_instance, only: :create
|
19
28
|
before_filter :assign_existing_instance, only: [ :show, :update, :destroy ]
|
20
29
|
before_filter :assign_collection, only: [ :index, :count ]
|
21
30
|
before_filter :did_assign_collection, only: [ :index, :count ]
|
22
|
-
|
31
|
+
|
23
32
|
define_method("index") do
|
24
33
|
respond_to do |format|
|
25
|
-
format.json { render json: @collection.to_json(merge_options(
|
26
|
-
format.xml { render xml: @collection.to_xml(merge_options(
|
34
|
+
format.json { render json: @collection.to_json(merge_options(defaults[:index])) }
|
35
|
+
format.xml { render xml: @collection.to_xml(merge_options(defaults[:index])) }
|
27
36
|
end
|
28
37
|
end
|
29
38
|
|
@@ -48,8 +57,8 @@ module Serviceable
|
|
48
57
|
|
49
58
|
define_method("show") do
|
50
59
|
respond_to do |format|
|
51
|
-
format.json { render json: @instance.to_json(merge_options(
|
52
|
-
format.xml { render xml: @instance.to_xml(merge_options(
|
60
|
+
format.json { render json: @instance.to_json(merge_options(defaults[:show])) }
|
61
|
+
format.xml { render xml: @instance.to_xml(merge_options(defaults[:show])) }
|
53
62
|
end
|
54
63
|
end
|
55
64
|
|
@@ -73,23 +82,107 @@ module Serviceable
|
|
73
82
|
format.xml { head :no_content }
|
74
83
|
end
|
75
84
|
end
|
85
|
+
|
86
|
+
define_method("describe") do
|
87
|
+
details = {
|
88
|
+
allowed_includes: force_array(defaults[:allowed_includes]),
|
89
|
+
allowed_methods: force_array(defaults[:allowed_methods])
|
90
|
+
}
|
91
|
+
respond_to do |format|
|
92
|
+
format.json { render json: details.to_json, status: :ok }
|
93
|
+
format.xml { render xml: details.to_xml, status: :ok }
|
94
|
+
end
|
95
|
+
end
|
76
96
|
|
77
97
|
# query string params can be given in the following formats:
|
78
98
|
# only=field1,field2
|
79
99
|
# except=field1,field2
|
80
100
|
# include=assoc1
|
101
|
+
# methods=my_helper
|
81
102
|
#
|
82
103
|
# if an included association is present, only and except params can be nested
|
83
104
|
# include[user][except]=encrypted_password
|
84
105
|
# include[user][only][]=first_name&include[user][only][]=last_name
|
85
106
|
# include[user][only]=first_name,last_name
|
107
|
+
#
|
108
|
+
# NOTE: includes and methods are not supported for nested associations
|
109
|
+
#
|
110
|
+
# options specified by the developer are considered mandatory and can not be
|
111
|
+
# overridden by the client
|
112
|
+
#
|
113
|
+
# client may only use includes and methods that are explicitly enabled by
|
114
|
+
# the developer
|
86
115
|
define_method("merge_options") do |options={}|
|
87
|
-
merged_options =
|
88
|
-
for key in [:only, :except
|
116
|
+
merged_options = {}
|
117
|
+
for key in [:only, :except]
|
89
118
|
opts = {key => params[key]} if params[key]
|
90
119
|
merged_options = merged_options.merge(opts) if opts
|
91
120
|
end
|
92
|
-
|
121
|
+
if params[:include].kind_of?(Hash)
|
122
|
+
requested_includes = params[:include]
|
123
|
+
elsif params[:include].kind_of?(Array)
|
124
|
+
requested_includes = Hash[params[:include].map {|e| [e,{}]}]
|
125
|
+
elsif params[:include].kind_of?(String)
|
126
|
+
requested_includes = Hash[params[:include].split(',').map {|e| [e,{}]}]
|
127
|
+
else
|
128
|
+
requested_includes = {}
|
129
|
+
end
|
130
|
+
if defaults[:allowed_includes].kind_of?(Hash)
|
131
|
+
allowed_includes = defaults[:allowed_includes]
|
132
|
+
elsif defaults[:allowed_includes].kind_of?(Array)
|
133
|
+
allowed_includes = Hash[defaults[:allowed_includes].map {|e| [e,{}]}]
|
134
|
+
elsif defaults[:allowed_includes].kind_of?(String)
|
135
|
+
allowed_includes = Hash[defaults[:allowed_includes].split(',').map {|e| [e,{}]}]
|
136
|
+
else
|
137
|
+
allowed_includes = {}
|
138
|
+
end
|
139
|
+
requested_includes = deep_sym(requested_includes)
|
140
|
+
allowed_includes = deep_sym(allowed_includes)
|
141
|
+
whitelisted_includes = {}
|
142
|
+
requested_includes.keys.each do |k|
|
143
|
+
if allowed_includes.keys.include?(k)
|
144
|
+
values = requested_includes[k]
|
145
|
+
opts = {}
|
146
|
+
opts[:only] = values[:only] if values[:only]
|
147
|
+
opts[:except] = values[:except] if values[:except]
|
148
|
+
whitelisted_includes[k] = opts
|
149
|
+
end
|
150
|
+
end
|
151
|
+
if options && options[:include]
|
152
|
+
if options[:include].kind_of?(Hash)
|
153
|
+
mandatory_includes = options[:include]
|
154
|
+
elsif options[:include].kind_of?(Array)
|
155
|
+
mandatory_includes = Hash[options[:include].map {|e| [e,{}]}]
|
156
|
+
else
|
157
|
+
mandatory_includes = {options[:include] => {}}
|
158
|
+
end
|
159
|
+
whitelisted_includes = whitelisted_includes.merge(mandatory_includes)
|
160
|
+
end
|
161
|
+
merged_options = merged_options.merge({include: whitelisted_includes}) if whitelisted_includes.keys.any?
|
162
|
+
|
163
|
+
if params[:methods].kind_of?(Hash)
|
164
|
+
requested_methods = params[:methods].keys
|
165
|
+
elsif params[:methods].kind_of?(Array)
|
166
|
+
requested_methods = params[:methods]
|
167
|
+
elsif params[:methods].kind_of?(String)
|
168
|
+
requested_methods = params[:methods].split(',')
|
169
|
+
else
|
170
|
+
requested_methods = []
|
171
|
+
end
|
172
|
+
if defaults[:allowed_methods].kind_of?(Hash)
|
173
|
+
allowed_methods = defaults[:allowed_methods].keys
|
174
|
+
elsif defaults[:allowed_methods].kind_of?(Array)
|
175
|
+
allowed_methods = defaults[:allowed_methods]
|
176
|
+
elsif defaults[:allowed_methods].kind_of?(String)
|
177
|
+
allowed_methods = defaults[:allowed_methods].split(',')
|
178
|
+
else
|
179
|
+
allowed_methods = []
|
180
|
+
end
|
181
|
+
requested_methods = requested_methods.map(&:to_sym)
|
182
|
+
allowed_methods = allowed_methods.map(&:to_sym)
|
183
|
+
whitelisted_methods = requested_methods & allowed_methods
|
184
|
+
merged_options = merged_options.merge({methods: whitelisted_methods}) if whitelisted_methods.any?
|
185
|
+
merged_options = deep_split(merged_options.compact)
|
93
186
|
return merged_options
|
94
187
|
end
|
95
188
|
|
@@ -163,7 +256,19 @@ module Serviceable
|
|
163
256
|
|
164
257
|
# designed to traverse an entire hash, replacing delimited strings with arrays of symbols
|
165
258
|
define_method("deep_split") do |hash={},pivot=','|
|
166
|
-
Hash[hash.map {|k,v| [k.to_sym,v.kind_of?(String) ? v.split(pivot).map(&:to_sym) : (v.kind_of?(Hash) ? deep_split(v,pivot) : v)]}]
|
259
|
+
Hash[hash.reject {|k,v| k.nil? || v.nil?}.map {|k,v| [k.to_sym,v.kind_of?(String) ? v.split(pivot).compact.map(&:to_sym) : (v.kind_of?(Hash) ? deep_split(v,pivot) : v)]}]
|
260
|
+
end
|
261
|
+
|
262
|
+
define_method("deep_sym") do |hash={}|
|
263
|
+
Hash[hash.reject {|k,v| k.nil? || v.nil?}.map {|k,v| [k.to_sym,v.kind_of?(String) ? v.to_sym : (v.kind_of?(Hash) ? deep_sym(v) : (v.kind_of?(Array) ? v.compact.map(&:to_sym) : v))]}]
|
264
|
+
end
|
265
|
+
|
266
|
+
define_method("force_array") do |obj|
|
267
|
+
obj.kind_of?(Array) ? obj : (obj.kind_of?(Hash) ? obj.keys : (obj==nil ? [] : [obj]))
|
268
|
+
end
|
269
|
+
|
270
|
+
define_method("required_fields") do
|
271
|
+
object.to_s.capitalize.constantize.accessible_attributes.select {|e| is_required_column?(e)}
|
167
272
|
end
|
168
273
|
|
169
274
|
define_method("is_time_column?") do |column|
|
@@ -173,6 +278,10 @@ module Serviceable
|
|
173
278
|
define_method("is_boolean_column?") do |column|
|
174
279
|
object.to_s.capitalize.constantize.columns.select {|e| e.name==column.to_s}.first.type == :boolean rescue false
|
175
280
|
end
|
281
|
+
|
282
|
+
define_method("is_required_column?") do |column|
|
283
|
+
object.to_s.capitalize.constantize.validators_on(column).map(&:class).include?(ActiveModel::Validations::PresenceValidator)
|
284
|
+
end
|
176
285
|
end
|
177
286
|
|
178
287
|
end
|
metadata
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: serviceable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 7
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: "0.
|
8
|
+
- 6
|
9
|
+
version: "0.6"
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Aubrey Goodman
|
@@ -14,10 +14,10 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2013-
|
17
|
+
date: 2013-09-03 00:00:00 Z
|
18
18
|
dependencies: []
|
19
19
|
|
20
|
-
description: Decorate your controller classes with acts_as_service :model_name, and instantly support JSON/XML CRUD interface.
|
20
|
+
description: Decorate your controller classes with acts_as_service :model_name, and instantly support JSON/XML CRUD interface. Allow client to specify response contents using query string filter parameters.
|
21
21
|
email: aubrey.goodman@gmail.com
|
22
22
|
executables: []
|
23
23
|
|