weasel_diesel 1.0.0
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/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
|