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.
Files changed (3) hide show
  1. data/README.md +32 -2
  2. data/lib/serviceable.rb +119 -10
  3. metadata +5 -5
data/README.md CHANGED
@@ -13,10 +13,18 @@ Controller:
13
13
 
14
14
  end
15
15
 
16
- Route:
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> param to specify fields on the collection
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
@@ -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,options={})
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(options[:index])) }
26
- format.xml { render xml: @collection.to_xml(merge_options(options[:index])) }
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(options[:show])) }
52
- format.xml { render xml: @instance.to_xml(merge_options(options[:show])) }
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 = options || {}
88
- for key in [:only, :except, :include, :methods]
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
- merged_options = deep_split(merged_options)
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: 1
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 5
9
- version: "0.5"
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-08-26 00:00:00 Z
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. Override any callback method to customize your endpoint. Allow client to specify response contents using query string filter parameters.
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