serviceable 0.5 → 0.6
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 +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
|
|