weasel_diesel 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +23 -0
- data/README.md +231 -0
- data/Rakefile +12 -0
- data/lib/documentation.rb +151 -0
- data/lib/framework_ext/sinatra.rb +30 -0
- data/lib/framework_ext/sinatra_controller.rb +80 -0
- data/lib/inflection.rb +460 -0
- data/lib/json_response_verification.rb +109 -0
- data/lib/params.rb +374 -0
- data/lib/params_verification.rb +268 -0
- data/lib/response.rb +457 -0
- data/lib/weasel_diesel.rb +415 -0
- data/lib/weasel_diesel/version.rb +3 -0
- data/lib/ws_list.rb +58 -0
- data/spec/hello_world_controller.rb +5 -0
- data/spec/hello_world_service.rb +20 -0
- data/spec/json_response_description_spec.rb +124 -0
- data/spec/json_response_verification_spec.rb +225 -0
- data/spec/params_verification_spec.rb +102 -0
- data/spec/preferences_service.rb +10 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/test_services.rb +102 -0
- data/spec/wsdsl_sinatra_ext_spec.rb +26 -0
- data/spec/wsdsl_spec.rb +314 -0
- data/weasel_diesel.gemspec +30 -0
- metadata +127 -0
@@ -0,0 +1,268 @@
|
|
1
|
+
# ParamsVerification module.
|
2
|
+
# Written to verify a service params without creating new objects.
|
3
|
+
# This module is used on all requests requiring validation and therefore performance
|
4
|
+
# security and maintainability are critical.
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
module ParamsVerification
|
8
|
+
|
9
|
+
class ParamError < StandardError; end #:nodoc
|
10
|
+
class NoParamsDefined < ParamError; end #:nodoc
|
11
|
+
class MissingParam < ParamError; end #:nodoc
|
12
|
+
class UnexpectedParam < ParamError; end #:nodoc
|
13
|
+
class InvalidParamType < ParamError; end #:nodoc
|
14
|
+
class InvalidParamValue < ParamError; end #:nodoc
|
15
|
+
|
16
|
+
# An array of validation regular expressions.
|
17
|
+
# The array gets cached but can be accessed via the symbol key.
|
18
|
+
#
|
19
|
+
# @return [Hash] An array with all the validation types as keys and regexps as values.
|
20
|
+
# @api public
|
21
|
+
def self.type_validations
|
22
|
+
@type_validations ||= { :integer => /^-?\d+$/,
|
23
|
+
:float => /^-?(\d*\.\d+|\d+)$/,
|
24
|
+
:decimal => /^-?(\d*\.\d+|\d+)$/,
|
25
|
+
:datetime => /^[-\d:T\s\+]+$/, # "T" is for ISO date format
|
26
|
+
:boolean => /^(1|true|TRUE|T|Y|0|false|FALSE|F|N)$/,
|
27
|
+
#:array => /,/
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Validation against each required WeaselDiesel::Params::Rule
|
32
|
+
# and returns the potentially modified params (with default values)
|
33
|
+
#
|
34
|
+
# @param [Hash] params The params to verify (incoming request params)
|
35
|
+
# @param [WeaselDiesel::Params] service_params A Playco service param compatible object listing required and optional params
|
36
|
+
# @param [Boolean] ignore_unexpected Flag letting the validation know if unexpected params should be ignored
|
37
|
+
#
|
38
|
+
# @return [Hash]
|
39
|
+
# The passed params potentially modified by the default rules defined in the service.
|
40
|
+
#
|
41
|
+
# @example Validate request params against a service's defined param rules
|
42
|
+
# ParamsVerification.validate!(request.params, @service.defined_params)
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def self.validate!(params, service_params, ignore_unexpected=false)
|
46
|
+
|
47
|
+
# Verify that no garbage params are passed, if they are, an exception is raised.
|
48
|
+
# only the first level is checked at this point
|
49
|
+
unless ignore_unexpected
|
50
|
+
unexpected_params?(params, service_params.param_names)
|
51
|
+
end
|
52
|
+
|
53
|
+
# dupe the params so we don't modify the passed value
|
54
|
+
updated_params = params.dup
|
55
|
+
# Required param verification
|
56
|
+
service_params.list_required.each do |rule|
|
57
|
+
updated_params = validate_required_rule(rule, updated_params)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Set optional defaults if any optional
|
61
|
+
service_params.list_optional.each do |rule|
|
62
|
+
updated_params = run_optional_rule(rule, updated_params)
|
63
|
+
end
|
64
|
+
|
65
|
+
# check the namespaced params
|
66
|
+
service_params.namespaced_params.each do |param|
|
67
|
+
param.list_required.each do |rule|
|
68
|
+
updated_params = validate_required_rule(rule, updated_params, param.space_name.to_s)
|
69
|
+
end
|
70
|
+
param.list_optional.each do |rule|
|
71
|
+
updated_params = run_optional_rule(rule, updated_params, param.space_name.to_s)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
# verify nested params, only 1 level deep tho
|
77
|
+
params.each_pair do |key, value|
|
78
|
+
if value.is_a?(Hash)
|
79
|
+
namespaced = service_params.namespaced_params.find{|np| np.space_name.to_s == key.to_s}
|
80
|
+
raise UnexpectedParam, "Request included unexpected parameter: #{key}" if namespaced.nil?
|
81
|
+
unexpected_params?(params[key], namespaced.param_names)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
updated_params
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Validate a required rule against a list of params passed.
|
92
|
+
#
|
93
|
+
#
|
94
|
+
# @param [WeaselDiesel::Params::Rule] rule The required rule to check against.
|
95
|
+
# @param [Hash] params The request params.
|
96
|
+
# @param [String] namespace Optional param namespace to check the rule against.
|
97
|
+
#
|
98
|
+
# @return [Hash]
|
99
|
+
# A hash representing the potentially modified params after going through the filter.
|
100
|
+
#
|
101
|
+
# @api private
|
102
|
+
def self.validate_required_rule(rule, params, namespace=nil)
|
103
|
+
param_name = rule.name.to_s
|
104
|
+
|
105
|
+
param_value, namespaced_params = extract_param_values(params, param_name, namespace)
|
106
|
+
# puts "verify #{param_name} params, current value: #{param_value}"
|
107
|
+
|
108
|
+
#This is disabled since required params shouldn't have a default, otherwise, why are they required?
|
109
|
+
#if param_value.nil? && rule.options && rule.options[:default]
|
110
|
+
#param_value = rule.options[:default]
|
111
|
+
#end
|
112
|
+
|
113
|
+
# Checks presence
|
114
|
+
if !(namespaced_params || params).keys.include?(param_name)
|
115
|
+
raise MissingParam, "'#{rule.name}' is missing - passed params: #{params.inspect}."
|
116
|
+
# checks null
|
117
|
+
elsif param_value.nil? && !rule.options[:null]
|
118
|
+
raise InvalidParamValue, "Value for parameter '#{param_name}' is missing - passed params: #{params.inspect}."
|
119
|
+
# checks type
|
120
|
+
elsif rule.options[:type]
|
121
|
+
verify_cast(param_name, param_value, rule.options[:type])
|
122
|
+
end
|
123
|
+
|
124
|
+
if rule.options[:options] || rule.options[:in]
|
125
|
+
choices = rule.options[:options] || rule.options[:in]
|
126
|
+
if rule.options[:type]
|
127
|
+
# Force the cast so we can compare properly
|
128
|
+
param_value = params[param_name] = type_cast_value(rule.options[:type], param_value)
|
129
|
+
end
|
130
|
+
raise InvalidParamValue, "Value for parameter '#{param_name}' (#{param_value}) is not in the allowed set of values." unless choices.include?(param_value)
|
131
|
+
# You can have a "in" rule that also applies a min value since they are mutually exclusive
|
132
|
+
elsif rule.options[:minvalue]
|
133
|
+
min = rule.options[:minvalue]
|
134
|
+
raise InvalidParamValue, "Value for parameter '#{param_name}' is lower than the min accepted value (#{min})." if param_value.to_i < min
|
135
|
+
end
|
136
|
+
# Returns the updated params
|
137
|
+
|
138
|
+
# cast the type if a type is defined and if a range of options isn't defined since the casting should have been done already
|
139
|
+
if rule.options[:type] && !(rule.options[:options] || rule.options[:in])
|
140
|
+
# puts "casting #{param_value} into type: #{rule.options[:type]}"
|
141
|
+
params[param_name] = type_cast_value(rule.options[:type], param_value)
|
142
|
+
end
|
143
|
+
params
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
# Extract the param valie and the namespaced params
|
148
|
+
# based on a passed namespace and params
|
149
|
+
#
|
150
|
+
# @param [Hash] params The passed params to extract info from.
|
151
|
+
# @param [String] param_name The param name to find the value.
|
152
|
+
# @param [NilClass, String] namespace the params' namespace.
|
153
|
+
# @return [Arrays<Object, String>]
|
154
|
+
#
|
155
|
+
# @api private
|
156
|
+
def self.extract_param_values(params, param_name, namespace=nil)
|
157
|
+
# Namespace check
|
158
|
+
if namespace == '' || namespace.nil?
|
159
|
+
[params[param_name], nil]
|
160
|
+
else
|
161
|
+
# puts "namespace: #{namespace} - params #{params[namespace].inspect}"
|
162
|
+
namespaced_params = params[namespace]
|
163
|
+
if namespaced_params
|
164
|
+
[namespaced_params[param_name], namespaced_params]
|
165
|
+
else
|
166
|
+
[nil, namespaced_params]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# @param [#WeaselDiesel::Params::Rule] rule The optional rule
|
172
|
+
# @param [Hash] params The request params
|
173
|
+
# @param [String] namespace An optional namespace
|
174
|
+
# @return [Hash] The potentially modified params
|
175
|
+
#
|
176
|
+
# @api private
|
177
|
+
def self.run_optional_rule(rule, params, namespace=nil)
|
178
|
+
param_name = rule.name.to_s
|
179
|
+
|
180
|
+
param_value, namespaced_params = extract_param_values(params, param_name, namespace)
|
181
|
+
|
182
|
+
if param_value.nil? && rule.options[:default]
|
183
|
+
if namespace
|
184
|
+
params[namespace][param_name] = param_value = rule.options[:default]
|
185
|
+
else
|
186
|
+
params[param_name] = param_value = rule.options[:default]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# cast the type if a type is defined and if a range of options isn't defined since the casting should have been done already
|
191
|
+
if rule.options[:type] && !param_value.nil?
|
192
|
+
if namespace
|
193
|
+
params[namespace][param_name] = param_value = type_cast_value(rule.options[:type], param_value)
|
194
|
+
else
|
195
|
+
params[param_name] = param_value = type_cast_value(rule.options[:type], param_value)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
choices = rule.options[:options] || rule.options[:in]
|
200
|
+
if choices && param_value && !choices.include?(param_value)
|
201
|
+
raise InvalidParamValue, "Value for parameter '#{param_name}' (#{param_value}) is not in the allowed set of values."
|
202
|
+
end
|
203
|
+
|
204
|
+
params
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.unexpected_params?(params, param_names)
|
208
|
+
# Raise an exception unless no unexpected params were found
|
209
|
+
unexpected_keys = (params.keys - param_names)
|
210
|
+
unless unexpected_keys.empty?
|
211
|
+
raise UnexpectedParam, "Request included unexpected parameter(s): #{unexpected_keys.join(', ')}"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
def self.type_cast_value(type, value)
|
217
|
+
case type
|
218
|
+
when :integer
|
219
|
+
value.to_i
|
220
|
+
when :float, :decimal
|
221
|
+
value.to_f
|
222
|
+
when :string
|
223
|
+
value.to_s
|
224
|
+
when :boolean
|
225
|
+
if value.is_a? TrueClass
|
226
|
+
true
|
227
|
+
elsif value.is_a? FalseClass
|
228
|
+
false
|
229
|
+
else
|
230
|
+
case value.to_s
|
231
|
+
when /^(1|true|TRUE|T|Y)$/
|
232
|
+
true
|
233
|
+
when /^(0|false|FALSE|F|N)$/
|
234
|
+
false
|
235
|
+
else
|
236
|
+
raise InvalidParamValue, "Could not typecast boolean to appropriate value"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
# An array type is a comma delimited string, we need to cast the passed strings.
|
240
|
+
when :array
|
241
|
+
value.respond_to?(:split) ? value.split(',') : value
|
242
|
+
when :binary, :array, :file
|
243
|
+
value
|
244
|
+
else
|
245
|
+
value
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Checks that the value's type matches the expected type for a given param
|
250
|
+
#
|
251
|
+
# @param [Symbol, String] Param name used if the verification fails and that an error is raised.
|
252
|
+
# @param [#to_s] The value to validate.
|
253
|
+
# @param [Symbol] The expected type, such as :boolean, :integer etc...
|
254
|
+
# @raise [InvalidParamType] Custom exception raised when the validation isn't found or the value doesn't match.
|
255
|
+
#
|
256
|
+
# @return [Nil]
|
257
|
+
# @api public
|
258
|
+
# TODO raising an exception really isn't a good idea since it forces the stack to unwind.
|
259
|
+
# More than likely developers are using exceptions to control the code flow and a different approach should be used.
|
260
|
+
# Catch/throw is a bit more efficient but is still the wrong approach for this specific problem.
|
261
|
+
def self.verify_cast(name, value, expected_type)
|
262
|
+
validation = ParamsVerification.type_validations[expected_type.to_sym]
|
263
|
+
unless validation.nil? || value.to_s =~ validation
|
264
|
+
raise InvalidParamType, "Value for parameter '#{name}' (#{value}) is of the wrong type (expected #{expected_type})"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
data/lib/response.rb
ADDED
@@ -0,0 +1,457 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class WeaselDiesel
|
4
|
+
# Response DSL class
|
5
|
+
# @api public
|
6
|
+
class Response
|
7
|
+
|
8
|
+
# The list of all the elements inside the response
|
9
|
+
#
|
10
|
+
# @return [Array<WeaselDiesel::Response::Element>]
|
11
|
+
# @api public
|
12
|
+
attr_reader :elements
|
13
|
+
|
14
|
+
# The list of all the arays inside the response
|
15
|
+
#
|
16
|
+
# @return [Array<WeaselDiesel::Response::Array>]
|
17
|
+
attr_reader :arrays
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@elements = []
|
21
|
+
@arrays = []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Lists all top level simple elements and array elements.
|
25
|
+
#
|
26
|
+
# @return [Array<WeaselDiesel::Response::Element, WeaselDiesel::Response::Array>]
|
27
|
+
def nodes
|
28
|
+
elements + arrays
|
29
|
+
end
|
30
|
+
|
31
|
+
# Shortcut to automatically create a node of array type.
|
32
|
+
# Useful when describing a JSON response.
|
33
|
+
#
|
34
|
+
# @param [String, Symbol] name the name of the element.
|
35
|
+
# @param [Hash] opts the element options.
|
36
|
+
# @see Vector#initialize
|
37
|
+
def array(name, type=nil)
|
38
|
+
vector = Vector.new(name, type)
|
39
|
+
yield(vector) if block_given?
|
40
|
+
@arrays << vector
|
41
|
+
end
|
42
|
+
|
43
|
+
# Defines a new element and yields the content of an optional block
|
44
|
+
# Each new element is then stored in the elements array.
|
45
|
+
#
|
46
|
+
# @param [Hash] opts Options used to define the element
|
47
|
+
# @option opts [String, Symbol] :name The element name
|
48
|
+
# @option opts [String, Symbol] :type The optional type
|
49
|
+
#
|
50
|
+
# @yield [WeaselDiesel::Response::Element] the newly created element
|
51
|
+
# @example create an element called 'my_stats'.
|
52
|
+
# service.response do |response|
|
53
|
+
# response.element(:name => "my_stats", :type => 'Leaderboard')
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# @return [WeaselDiesel::Response::Element]
|
57
|
+
# @api public
|
58
|
+
def element(opts={})
|
59
|
+
el = Element.new(opts[:name], opts[:type])
|
60
|
+
yield(el) if block_given?
|
61
|
+
@elements << el
|
62
|
+
el
|
63
|
+
end
|
64
|
+
|
65
|
+
# Defines an anonymous element
|
66
|
+
# Useful for JSON response description
|
67
|
+
#
|
68
|
+
# @return [WeaselDiesel::Response::Element]
|
69
|
+
def object
|
70
|
+
yield element
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns a response element object based on its name
|
74
|
+
# @param [String, Symbol] The element name we want to match
|
75
|
+
#
|
76
|
+
# @return [WeaselDiesel::Response::Element]
|
77
|
+
# @api public
|
78
|
+
def element_named(name)
|
79
|
+
@elements.find{|e| e.name.to_s == name.to_s}
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Converts the object into a JSON representation
|
84
|
+
# @return [String] JSON representation of the response
|
85
|
+
def to_json(*args)
|
86
|
+
if nodes.size > 1
|
87
|
+
nodes.to_json(*args)
|
88
|
+
else
|
89
|
+
nodes.first.to_json(*args)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
class Params
|
95
|
+
class Rule
|
96
|
+
def to_hash
|
97
|
+
{:name => name, :options => options}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# The Response element class describing each element of a service response.
|
103
|
+
# Instances are usually not instiated directly but via the Response#element accessor.
|
104
|
+
#
|
105
|
+
# @see WeaselDiesel::Response#element
|
106
|
+
# @api public
|
107
|
+
class Element
|
108
|
+
|
109
|
+
# @return [String, #to_s] The name of the element
|
110
|
+
# @api public
|
111
|
+
attr_reader :name
|
112
|
+
|
113
|
+
# @api public
|
114
|
+
attr_reader :type
|
115
|
+
|
116
|
+
# The optional lookup key of an object
|
117
|
+
attr_reader :key
|
118
|
+
|
119
|
+
# @return [Array<WeaselDiesel::Response::Element::Attribute>] An array of attributes
|
120
|
+
# @api public
|
121
|
+
attr_reader :attributes
|
122
|
+
|
123
|
+
# @return [Array<WeaselDiesel::Response::Element::MetaAttribute>] An array of meta attributes
|
124
|
+
# @api public
|
125
|
+
attr_reader :meta_attributes
|
126
|
+
|
127
|
+
# @return [Array] An array of vectors/arrays
|
128
|
+
# @api public
|
129
|
+
attr_reader :vectors
|
130
|
+
|
131
|
+
# @return [WeaselDiesel::Documentation::ElementDoc] Response element documentation
|
132
|
+
# @api public
|
133
|
+
attr_reader :doc
|
134
|
+
|
135
|
+
# @return [NilClass, Array<WeaselDiesel::Response::Element>] The optional nested elements
|
136
|
+
attr_reader :elements
|
137
|
+
|
138
|
+
# Alias to use a JSON/JS jargon instead of XML.
|
139
|
+
alias :properties :attributes
|
140
|
+
|
141
|
+
# Alias to use a JSON/JS jargon instead of XML.
|
142
|
+
alias :objects :elements
|
143
|
+
|
144
|
+
# param [String, Symbol] name The name of the element
|
145
|
+
# param [String, Symbol] type The optional type of the element
|
146
|
+
# @api public
|
147
|
+
def initialize(name, type=nil)
|
148
|
+
# sets a documentation placeholder since the response doc is defined at the same time
|
149
|
+
# the response is defined.
|
150
|
+
@doc = Documentation::ElementDoc.new(name)
|
151
|
+
@name = name
|
152
|
+
@type = type
|
153
|
+
@attributes = []
|
154
|
+
@meta_attributes = []
|
155
|
+
@vectors = []
|
156
|
+
@key = nil
|
157
|
+
# we don't need to initialize the nested elements, by default they should be nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# sets a new attribute and returns the entire list of attributes
|
161
|
+
#
|
162
|
+
# @param [Hash] opts An element's attribute options
|
163
|
+
# @option opts [String, Symbol] attribute_name The name of the attribute, the value being the type
|
164
|
+
# @option opts [String, Symbol] :doc The attribute documentation
|
165
|
+
# @option opts [String, Symbol] :mock An optional mock value used by service related tools
|
166
|
+
#
|
167
|
+
# @example Creation of a response attribute called 'best_lap_time'
|
168
|
+
# service.response do |response|
|
169
|
+
# response.element(:name => "my_stats", :type => 'Leaderboard') do |e|
|
170
|
+
# e.attribute "best_lap_time" => :float, :doc => "Best lap time in seconds."
|
171
|
+
# end
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# @return [Array<WeaselDiesel::Response::Attribute>]
|
175
|
+
# @api public
|
176
|
+
def attribute(opts, extra_opts={})
|
177
|
+
raise ArgumentError unless opts.is_a?(Hash) && extra_opts.is_a?(Hash)
|
178
|
+
new_attribute = Attribute.new(opts, extra_opts)
|
179
|
+
@attributes << new_attribute
|
180
|
+
# document the attribute if description available
|
181
|
+
# we might want to have a placeholder message when a response attribute isn't defined
|
182
|
+
if opts.merge!(extra_opts).has_key?(:doc)
|
183
|
+
@doc.attribute(new_attribute.name, opts[:doc])
|
184
|
+
end
|
185
|
+
@attributes
|
186
|
+
end
|
187
|
+
|
188
|
+
# sets a new meta attribute and returns the entire list of meta attributes
|
189
|
+
#
|
190
|
+
# @param [Hash] opts An element's attribute options
|
191
|
+
# @option opts [String, Symbol] attribute_name The name of the attribute, the value being the type
|
192
|
+
# @option opts [String, Symbol] :mock An optional mock value used by service related tools
|
193
|
+
#
|
194
|
+
# @example Creation of a response attribute called 'best_lap_time'
|
195
|
+
# service.response do |response|
|
196
|
+
# response.element(:name => "my_stats", :type => 'Leaderboard') do |e|
|
197
|
+
# e.meta_attribute "id" => :key
|
198
|
+
# end
|
199
|
+
# end
|
200
|
+
#
|
201
|
+
# @return [Array<WeaselDiesel::Response::MetaAttribute>]
|
202
|
+
# @api public
|
203
|
+
def meta_attribute(opts)
|
204
|
+
raise ArgumentError unless opts.is_a?(Hash)
|
205
|
+
# extract the documentation part and add it where it belongs
|
206
|
+
new_attribute = MetaAttribute.new(opts)
|
207
|
+
@meta_attributes << new_attribute
|
208
|
+
@meta_attributes
|
209
|
+
end
|
210
|
+
|
211
|
+
# Defines an array aka vector of elements.
|
212
|
+
#
|
213
|
+
# @param [String, Symbol] name The name of the array element.
|
214
|
+
# @param [String, Symbol] type Optional type information, useful to store the represented
|
215
|
+
# object types for instance.
|
216
|
+
#
|
217
|
+
# @param [Proc] &block
|
218
|
+
# A block to execute against the newly created array.
|
219
|
+
#
|
220
|
+
# @example Defining an element array called 'player_creation_rating'
|
221
|
+
# element.array 'player_creation_rating', 'PlayerCreationRating' do |a|
|
222
|
+
# a.attribute :comments => :string
|
223
|
+
# a.attribute :player_id => :integer
|
224
|
+
# a.attribute :rating => :integer
|
225
|
+
# a.attribute :username => :string
|
226
|
+
# end
|
227
|
+
# @yield [Vector] the newly created array/vector instance
|
228
|
+
# @see Element#initialize
|
229
|
+
#
|
230
|
+
# @return [Array<WeaselDiesel::Response::Vector>]
|
231
|
+
# @api public
|
232
|
+
def array(name, type=nil)
|
233
|
+
vector = Vector.new(name, type)
|
234
|
+
yield(vector) if block_given?
|
235
|
+
@vectors << vector
|
236
|
+
end
|
237
|
+
|
238
|
+
# Returns the arrays/vectors contained in the response.
|
239
|
+
# This is an alias to access @vectors
|
240
|
+
# @see @vectors
|
241
|
+
#
|
242
|
+
# @return [Array<WeaselDiesel::Response::Vector>]
|
243
|
+
# @api public
|
244
|
+
def arrays
|
245
|
+
@vectors
|
246
|
+
end
|
247
|
+
|
248
|
+
# Defines a new element and yields the content of an optional block
|
249
|
+
# Each new element is then stored in the elements array.
|
250
|
+
#
|
251
|
+
# @param [Hash] opts Options used to define the element
|
252
|
+
# @option opts [String, Symbol] :name The element name
|
253
|
+
# @option opts [String, Symbol] :type The optional type
|
254
|
+
#
|
255
|
+
# @yield [WeaselDiesel::Response::Element] the newly created element
|
256
|
+
# @example create an element called 'my_stats'.
|
257
|
+
# service.response do |response|
|
258
|
+
# response.element(:name => "my_stats", :type => 'Leaderboard')
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# @return [Array<WeaselDiesel::Response::Element>]
|
262
|
+
# @api public
|
263
|
+
def element(opts={})
|
264
|
+
el = Element.new(opts[:name], opts[:type])
|
265
|
+
yield(el) if block_given?
|
266
|
+
@elements ||= []
|
267
|
+
@elements << el
|
268
|
+
el
|
269
|
+
end
|
270
|
+
|
271
|
+
# Shortcut to create a new element.
|
272
|
+
#
|
273
|
+
# @param [Symbol, String] name the name of the element.
|
274
|
+
# @param [Hash] opts the options for the newly created element.
|
275
|
+
def object(name, opts={}, &block)
|
276
|
+
element(opts.merge(:name => name), &block)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Getter/setter for the key meta attribute.
|
280
|
+
# A key name can be used to lookup an object by a primary key for instance.
|
281
|
+
#
|
282
|
+
# @param [Symbol, String] name the name of the key attribute.
|
283
|
+
# @param [Hash] opts the options attached with the key.
|
284
|
+
def key(name=nil, opts={})
|
285
|
+
meta_attribute_getter_setter(:key, name, opts)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Getter/setter for the type meta attribute.
|
289
|
+
#
|
290
|
+
# @param [Symbol, String] name the name of the type attribute.
|
291
|
+
# @param [Hash] opts the options attached with the key.
|
292
|
+
def type(name=nil, opts={})
|
293
|
+
meta_attribute_getter_setter(:type, name, opts)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Shortcut to create a string attribute
|
297
|
+
#
|
298
|
+
# @param [Symbol, String] name the name of the attribute.
|
299
|
+
# @param [Hash] opts the attribute options.
|
300
|
+
def string(name=nil, opts={})
|
301
|
+
attribute({name => :string}, opts)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Shortcut to create a string attribute
|
305
|
+
#
|
306
|
+
# @param [Symbol, String] name the name of the attribute.
|
307
|
+
# @param [Hash] opts the attribute options.
|
308
|
+
def integer(name=nil, opts={})
|
309
|
+
attribute({name => :integer}, opts)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Shortcut to create a string attribute
|
313
|
+
#
|
314
|
+
# @param [Symbol, String] name the name of the attribute.
|
315
|
+
# @param [Hash] opts the attribute options.
|
316
|
+
def float(name=nil, opts={})
|
317
|
+
attribute({name => :float}, opts)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Shortcut to create a string attribute
|
321
|
+
#
|
322
|
+
# @param [Symbol, String] name the name of the attribute.
|
323
|
+
# @param [Hash] opts the attribute options.
|
324
|
+
def boolean(name=nil, opts={})
|
325
|
+
attribute({name => :boolean}, opts)
|
326
|
+
end
|
327
|
+
|
328
|
+
# Shortcut to create a string attribute
|
329
|
+
#
|
330
|
+
# @param [Symbol, String] name the name of the attribute.
|
331
|
+
# @param [Hash] opts the attribute options.
|
332
|
+
def datetime(name=nil, opts={})
|
333
|
+
attribute({name => :datetime}, opts)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Converts an element into a hash representation
|
337
|
+
#
|
338
|
+
# @return [Hash] the element attributes formated in a hash
|
339
|
+
def to_hash
|
340
|
+
attrs = {}
|
341
|
+
attributes.each{ |attr| attrs[attr.name] = attr.type }
|
342
|
+
elements.each{ |el| attrs[el.name] = el.to_hash } if elements
|
343
|
+
if self.class == Vector
|
344
|
+
name ? {name => [attrs]} : [attrs]
|
345
|
+
else
|
346
|
+
name ? {name => attrs} : attrs
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def to_html
|
351
|
+
output = ""
|
352
|
+
if name
|
353
|
+
output << "<li>"
|
354
|
+
output << "<span class='label notice'>#{name}</span> of type <span class='label success'>#{self.is_a?(Vector) ? 'Array' : 'Object'}</span>"
|
355
|
+
end
|
356
|
+
if self.is_a? Vector
|
357
|
+
output << "<h6>Properties of each array item:</h6>"
|
358
|
+
else
|
359
|
+
output << "<h6>Properties:</h6>"
|
360
|
+
end
|
361
|
+
output << "<ul>"
|
362
|
+
properties.each do |prop|
|
363
|
+
output << "<li><span class='label notice'>#{prop.name}</span> of type <span class='label success'>#{prop.type}</span> #{'(Can be blank or missing) ' if prop.opts && prop.opts.respond_to?(:[]) && prop.opts[:null]} "
|
364
|
+
output << prop.doc unless prop.doc.blank?
|
365
|
+
output << "</li>"
|
366
|
+
end
|
367
|
+
arrays.each{ |arr| output << arr.html }
|
368
|
+
elements.each {|el| output << el.to_html } if elements
|
369
|
+
output << "</ul>"
|
370
|
+
output << "</li>" if name
|
371
|
+
output
|
372
|
+
end
|
373
|
+
|
374
|
+
private
|
375
|
+
|
376
|
+
# Create a meta element attribute
|
377
|
+
def meta_attribute_getter_setter(type, name, opts)
|
378
|
+
if name
|
379
|
+
meta_attribute({name => type}.merge(opts))
|
380
|
+
else
|
381
|
+
# with a fallback to the @type ivar
|
382
|
+
meta = meta_attributes.find{|att| att.type == type}
|
383
|
+
if meta
|
384
|
+
meta.value
|
385
|
+
else
|
386
|
+
instance_variable_get("@#{type}")
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# Response element's attribute class
|
392
|
+
# @api public
|
393
|
+
class Attribute
|
394
|
+
|
395
|
+
# @return [String, #to_s] The attribute's name.
|
396
|
+
# @api public
|
397
|
+
attr_reader :name
|
398
|
+
alias :value :name
|
399
|
+
|
400
|
+
# @return [Symbol, String, #to_s] The attribute's type such as boolean, string etc..
|
401
|
+
# @api public
|
402
|
+
attr_reader :type
|
403
|
+
|
404
|
+
# @return [String] The documentation associated with this attribute.
|
405
|
+
# @api public
|
406
|
+
attr_reader :doc
|
407
|
+
|
408
|
+
# @see {Attribute#new}
|
409
|
+
# @return [Hash, Nil, Object] Could be a hash, nil or any object depending on how the attribute is created.
|
410
|
+
# @api public
|
411
|
+
attr_reader :opts
|
412
|
+
|
413
|
+
# Takes a Hash or an Array and extract the attribute name, type
|
414
|
+
# doc and extra options.
|
415
|
+
# If the passed objects is a Hash, the name will be extract from
|
416
|
+
# the first key and the type for the first value.
|
417
|
+
# An entry keyed by :doc will be used for the doc and the rest will go
|
418
|
+
# as extra options.
|
419
|
+
#
|
420
|
+
# If an Array is passed, the elements will be 'shifted' in this order:
|
421
|
+
# name, type, doc, type
|
422
|
+
#
|
423
|
+
# @param [Hash, Array] o_params
|
424
|
+
# @param [Hash] o_extra_params A hash with extra params passed, needed to support Ruby 1.8 :(
|
425
|
+
#
|
426
|
+
# @api public
|
427
|
+
def initialize(o_params, o_extra_params={})
|
428
|
+
params = o_params.dup
|
429
|
+
extra_params = o_extra_params.dup
|
430
|
+
if params.is_a?(Hash)
|
431
|
+
@name, @type = params.shift
|
432
|
+
@doc = params.delete(:doc) if params.has_key?(:doc)
|
433
|
+
@doc ||= extra_params.delete(:doc) if extra_params.has_key?(:doc)
|
434
|
+
@opts = params.merge!(extra_params)
|
435
|
+
elsif params.is_a?(Array)
|
436
|
+
@name = params.shift
|
437
|
+
@type = params.shift
|
438
|
+
@doc = params.shift
|
439
|
+
@opts = params
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Response's meta attribute meant to set some extra
|
445
|
+
# attributes which are not part of the response per se.
|
446
|
+
class MetaAttribute < Attribute
|
447
|
+
end
|
448
|
+
|
449
|
+
end # of Element
|
450
|
+
|
451
|
+
# Array of objects
|
452
|
+
# @api public
|
453
|
+
class Vector < Element
|
454
|
+
end # of Vector
|
455
|
+
|
456
|
+
end # of Response
|
457
|
+
end
|