serviceable 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
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