DistelliServiceMarshallers 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/distelli/servicemarshallers.rb +431 -0
- metadata +77 -0
@@ -0,0 +1,431 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'date'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'distelli/serviceinterface'
|
7
|
+
|
8
|
+
module Distelli
|
9
|
+
DATA_TYPES = ["int", "string", "boolean", "long", "double"]
|
10
|
+
class Marshaller
|
11
|
+
def initialize()
|
12
|
+
@object_map = Hash.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_object(obj)
|
16
|
+
@object_map[obj.name] = obj
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class MarshallException < StandardError
|
21
|
+
end
|
22
|
+
|
23
|
+
################################
|
24
|
+
# JSON Marshaller
|
25
|
+
################################
|
26
|
+
|
27
|
+
class JsonMarshaller < Marshaller
|
28
|
+
def initialize()
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def encode(obj)
|
33
|
+
data = Hash.new
|
34
|
+
if obj.is_a?(Integer) or obj.is_a?(Float) or obj.is_a?(String) or obj.is_a?(TrueClass) or obj.is_a?(FalseClass)
|
35
|
+
return obj
|
36
|
+
elsif obj.is_a?(DateTime)
|
37
|
+
return obj.strftime("%Y-%m-%dT%H:%M:%S%z")
|
38
|
+
end
|
39
|
+
|
40
|
+
type_map_name = '_'+obj.class.name.split('::').last+'__type_map'
|
41
|
+
obj.instance_variables.each do |var|
|
42
|
+
var_name = var.to_s.delete('@')
|
43
|
+
if var_name == type_map_name
|
44
|
+
next
|
45
|
+
end
|
46
|
+
val = obj.instance_variable_get(var)
|
47
|
+
if val.is_a?(Integer) or val.is_a?(Float) or val.is_a?(String) or val.is_a?(TrueClass) or val.is_a?(FalseClass)
|
48
|
+
data[var_name] = val
|
49
|
+
elsif val.is_a?(DateTime)
|
50
|
+
data[var_name] = val.strftime("%Y-%m-%dT%H:%M:%S%z")
|
51
|
+
elsif val.instance_of?(Array)
|
52
|
+
array = Array.new
|
53
|
+
val.each do |elem|
|
54
|
+
encoded = encode(elem)
|
55
|
+
array.push(encoded)
|
56
|
+
end
|
57
|
+
data[var_name] = array
|
58
|
+
else val.kind_of?(Object)
|
59
|
+
encoded = encode(val)
|
60
|
+
data[var_name] = encoded
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return data
|
64
|
+
end
|
65
|
+
|
66
|
+
def marshall(request)
|
67
|
+
data = encode(request)
|
68
|
+
request_wrapper = Hash.new
|
69
|
+
request_wrapper[request.class.name.split('::').last] = data
|
70
|
+
return request_wrapper.to_json
|
71
|
+
end
|
72
|
+
|
73
|
+
def marshall_error(error)
|
74
|
+
if not error.is_a?(Distelli::BaseException)
|
75
|
+
raise StandardError.new("Cannot marshall error: "+error.inspect)
|
76
|
+
end
|
77
|
+
|
78
|
+
err_msg = error.err_msg
|
79
|
+
err_code = error.err_code
|
80
|
+
return '{"Error":{"code":"'+err_code.to_s+'", "message":"'+err_msg.to_s+'"}}'
|
81
|
+
end
|
82
|
+
|
83
|
+
def unmarshall_error(json_data)
|
84
|
+
data_hash = JSON.load(json_data)
|
85
|
+
err_obj = data_hash[ServiceConstants::ERROR_KEY]
|
86
|
+
if err_obj == nil
|
87
|
+
return nil
|
88
|
+
end
|
89
|
+
err_code = err_obj[ServiceConstants::ERR_CODE_KEY]
|
90
|
+
err_msg = err_obj[ServiceConstants::ERR_MSG_KEY]
|
91
|
+
return [err_code, err_msg]
|
92
|
+
end
|
93
|
+
|
94
|
+
def unmarshall(json_data)
|
95
|
+
data_hash = JSON.load(json_data)
|
96
|
+
data_hash.each_pair do |k,v|
|
97
|
+
if v.is_a?(Hash)
|
98
|
+
return unmarshall_obj(k, v)
|
99
|
+
else
|
100
|
+
raise MarshallException.new("Invalid data "+json_data)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
def to_obj_list(obj_name, list_data)
|
107
|
+
obj_list = Array.new
|
108
|
+
# If its a list type then its a list of complex objects
|
109
|
+
if list_data.is_a?(Array)
|
110
|
+
list_data.each do |v|
|
111
|
+
obj = unmarshall_obj(obj_name, v)
|
112
|
+
obj_list.push(obj)
|
113
|
+
end
|
114
|
+
else # Else its a map (hopefully) and its only a single complex object
|
115
|
+
obj = unmarshall_obj(obj_name, list_data)
|
116
|
+
obj_list.push(obj)
|
117
|
+
end
|
118
|
+
return obj_list
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
def to_date_list(list_data)
|
123
|
+
date_list = Array.new
|
124
|
+
list_data.each do |v|
|
125
|
+
date_val = DateTime.strptime(v, "%Y-%m-%dT%H:%M:%S%z")
|
126
|
+
date_list.push(date_val)
|
127
|
+
end
|
128
|
+
return date_list
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
def unmarshall_list(list_data, data_type)
|
133
|
+
if list_data.length == 0
|
134
|
+
return Array.new
|
135
|
+
end
|
136
|
+
actual_list = list_data
|
137
|
+
if data_type == "date"
|
138
|
+
return to_date_list(actual_list)
|
139
|
+
elsif DATA_TYPES.include?(data_type)
|
140
|
+
return actual_list
|
141
|
+
else
|
142
|
+
return to_obj_list(data_type, actual_list)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
def unmarshall_obj(obj_name, data)
|
148
|
+
# puts "Unmarshalling object: "+obj_name
|
149
|
+
if !@object_map.has_key?(obj_name)
|
150
|
+
raise MarshallException.new("Unknown object: "+obj_name.to_s)
|
151
|
+
end
|
152
|
+
class_obj = @object_map[obj_name]
|
153
|
+
instance = class_obj.new
|
154
|
+
type_map_name = '@_'+obj_name+'__type_map'
|
155
|
+
type_map = instance.instance_variable_get(type_map_name)
|
156
|
+
data.each_pair do |k,v|
|
157
|
+
if !type_map.include?(k)
|
158
|
+
raise MarshallException.new("Unknown type for field: "+k)
|
159
|
+
end
|
160
|
+
field_type = type_map[k]
|
161
|
+
if field_type.is_a?(Array)
|
162
|
+
field_type = field_type[0]
|
163
|
+
end
|
164
|
+
if v.is_a?(Hash)
|
165
|
+
um_obj = unmarshall_obj(field_type, v)
|
166
|
+
instance.instance_variable_set('@'+k, um_obj)
|
167
|
+
elsif v.is_a?(Array)
|
168
|
+
um_list = unmarshall_list(v, field_type)
|
169
|
+
instance.instance_variable_set('@'+k, um_list)
|
170
|
+
else
|
171
|
+
if field_type == "date"
|
172
|
+
date_val = DateTime.strptime(v, "%Y-%m-%dT%H:%M:%S%z")
|
173
|
+
instance.instance_variable_set('@'+k, date_val)
|
174
|
+
elsif field_type == "int"
|
175
|
+
instance.instance_variable_set('@'+k, v.to_i)
|
176
|
+
elsif field_type == "double"
|
177
|
+
instance.instance_variable_set('@'+k, v.to_f)
|
178
|
+
elsif field_type == "long"
|
179
|
+
instance.instance_variable_set('@'+k, v.to_i)
|
180
|
+
else
|
181
|
+
instance.instance_variable_set('@'+k, v)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
return instance
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
################################
|
190
|
+
# XML Marshaller
|
191
|
+
################################
|
192
|
+
|
193
|
+
class XmlMarshaller < Marshaller
|
194
|
+
def initialize()
|
195
|
+
super
|
196
|
+
end
|
197
|
+
|
198
|
+
def marshall(request)
|
199
|
+
root_tag_name = request.class.name.split('::').last
|
200
|
+
return ['<', root_tag_name,'>', encode_obj_xml(request), '</', root_tag_name, '>'].join('')
|
201
|
+
end
|
202
|
+
|
203
|
+
def unmarshall(xml_data)
|
204
|
+
xml_doc = Nokogiri::XML(xml_data)
|
205
|
+
root_node = xml_doc.root
|
206
|
+
return unmarshall_obj(root_node)
|
207
|
+
end
|
208
|
+
|
209
|
+
def marshall_error(error)
|
210
|
+
if not error.is_a?(Distelli::BaseException)
|
211
|
+
raise StandardError.new("Cannot marshall error: "+error.inspect)
|
212
|
+
end
|
213
|
+
|
214
|
+
err_msg = error.err_msg
|
215
|
+
err_code = error.err_code
|
216
|
+
return "<Error><code>"+err_code.to_s+"</code><message>"+err_msg.to_s+"</message></Error>"
|
217
|
+
end
|
218
|
+
|
219
|
+
##########################################################
|
220
|
+
# Unmarshalls an xml error response and returns the error
|
221
|
+
# code and message.
|
222
|
+
#
|
223
|
+
# This is what an xml error looks like:
|
224
|
+
#
|
225
|
+
# <Error>
|
226
|
+
# <code>MalformedRequest</code>
|
227
|
+
# <message>The request is malformed</message>
|
228
|
+
# </Error>
|
229
|
+
##########################################################
|
230
|
+
def unmarshall_error(xml_data)
|
231
|
+
xml_doc = Nokogiri::XML(xml_data)
|
232
|
+
err_code_elem = xml_doc.xpath('//'+ServiceConstants::ERROR_KEY+'//'+ServiceConstants::ERR_CODE_KEY)
|
233
|
+
err_msg_elem = xml_doc.xpath('//'+ServiceConstants::ERROR_KEY+'//'+ServiceConstants::ERR_MSG_KEY)
|
234
|
+
err_code = nil
|
235
|
+
err_msg = nil
|
236
|
+
if err_code_elem != nil
|
237
|
+
err_code = err_code_elem.text
|
238
|
+
end
|
239
|
+
if err_msg_elem != nil
|
240
|
+
err_msg = err_msg_elem.text
|
241
|
+
end
|
242
|
+
return [err_code, err_msg]
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
def get_node_text(node)
|
247
|
+
children = node.children
|
248
|
+
if children.length == 0
|
249
|
+
return nil
|
250
|
+
end
|
251
|
+
if children.length != 1
|
252
|
+
raise MarshallException.new(node.to_s+" is not a text node")
|
253
|
+
end
|
254
|
+
|
255
|
+
text_node = children[0]
|
256
|
+
return text_node.content
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
def unmarshall_list(obj_node, list_type)
|
261
|
+
# puts "Unmarshalling list at "+obj_node.to_s
|
262
|
+
list_elements = obj_node.children
|
263
|
+
list_data = Array.new
|
264
|
+
list_elements.each do |elem|
|
265
|
+
# If its a simple type list
|
266
|
+
# puts "Unmarshalling list elemnt "+elem.name+" "+list_type
|
267
|
+
if list_type == "date"
|
268
|
+
obj_node_content = get_node_text(elem)
|
269
|
+
date_val = DateTime.strptime(obj_node.content, "%Y-%m-%dT%H:%M:%S%z")
|
270
|
+
list_data.push(date_val)
|
271
|
+
elsif DATA_TYPES.include?(list_type)
|
272
|
+
# puts "Unmarshalling primitive list element "+elem.name+" "+list_type
|
273
|
+
elem_text = get_node_text(elem)
|
274
|
+
if list_type == "int"
|
275
|
+
list_data.push(elem_text.to_i)
|
276
|
+
elsif list_type == "long"
|
277
|
+
list_data.push(elem_text.to_i)
|
278
|
+
elsif list_type == "boolean"
|
279
|
+
if elem_text.downcase == "true"
|
280
|
+
list_data.push(true)
|
281
|
+
else
|
282
|
+
list_data.push(false)
|
283
|
+
end
|
284
|
+
elsif list_type == "double"
|
285
|
+
list_data.push(elem_text.to_f)
|
286
|
+
else
|
287
|
+
list_data.push(elem_text)
|
288
|
+
end
|
289
|
+
else
|
290
|
+
# puts "Unmarshalling complex list element"+elem.name+" "+list_type
|
291
|
+
# Else its a list of complex types
|
292
|
+
unmarshalled_obj = unmarshall_obj(elem)
|
293
|
+
list_data.push(unmarshalled_obj)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
return list_data
|
297
|
+
end
|
298
|
+
|
299
|
+
private
|
300
|
+
def unmarshall_obj(obj_node, parent_obj=nil)
|
301
|
+
# puts "Unmarshalling "+obj_node.name+" "+obj_node.inspect+" member of "+parent_obj.to_s
|
302
|
+
obj_name = obj_node.name
|
303
|
+
obj_instance = nil
|
304
|
+
if parent_obj == nil
|
305
|
+
if !@object_map.has_key?(obj_name)
|
306
|
+
raise MarshallException.new("Unknown object: "+obj_name.to_s)
|
307
|
+
end
|
308
|
+
class_obj = @object_map[obj_name]
|
309
|
+
obj_instance = class_obj.new
|
310
|
+
else
|
311
|
+
# Parent object is not none. So obj_node is a member of
|
312
|
+
# obj_instance and we can get the type from the parent obj
|
313
|
+
# First get the type map
|
314
|
+
# puts "Processing "+obj_name
|
315
|
+
type_map_name = '@_'+parent_obj.class.name.split('::').last+'__type_map'
|
316
|
+
type_map = parent_obj.instance_variable_get(type_map_name)
|
317
|
+
if type_map == nil
|
318
|
+
raise MarshallException.new(parent_obj.class.name+" cannot be unmarshalled. Missing type_map: "+type_map_name)
|
319
|
+
end
|
320
|
+
|
321
|
+
if !type_map.include?(obj_name)
|
322
|
+
raise MarshallException.new("Unknown type for field: "+obj_name+" in obj "+obj_instance.to_s)
|
323
|
+
end
|
324
|
+
obj_type = type_map[obj_name]
|
325
|
+
# puts "Unmarshalling "+obj_name+" "+obj_type.to_s
|
326
|
+
# Obj type can be either a list or one of the defined
|
327
|
+
# primitive types or a complex type
|
328
|
+
if obj_type.is_a?(Array)
|
329
|
+
list_type = obj_type[0]
|
330
|
+
# puts obj_name+" is a list in "+parent_obj.to_s+" of type "+list_type
|
331
|
+
unmarshalled_list = unmarshall_list(obj_node, list_type)
|
332
|
+
# puts "Unmarshalled list"+unmarshalled_list.to_s
|
333
|
+
parent_obj.instance_variable_set('@'+obj_name, unmarshalled_list)
|
334
|
+
return obj_instance
|
335
|
+
elsif obj_type == "date"
|
336
|
+
date_val = DateTime.strptime(obj_node.content, "%Y-%m-%dT%H:%M:%S%z")
|
337
|
+
parent_obj.instance_variable_set('@'+obj_name, date_val)
|
338
|
+
return obj_instance
|
339
|
+
elsif DATA_TYPES.include?(obj_type)
|
340
|
+
# puts obj_name+" is a primitive "+obj_type+" "+obj_node+" in "+parent_obj.to_s
|
341
|
+
if obj_type == "int" or obj_type == "long"
|
342
|
+
parent_obj.instance_variable_set('@'+obj_name, get_node_text(obj_node).to_i)
|
343
|
+
elsif obj_type == "boolean"
|
344
|
+
obj_text = get_node_text(obj_node)
|
345
|
+
if obj_text.downcase == "true"
|
346
|
+
parent_obj.instance_variable_set('@'+obj_name, true)
|
347
|
+
else
|
348
|
+
parent_obj.instance_variable_set('@'+obj_name, false)
|
349
|
+
end
|
350
|
+
elsif obj_type == "double"
|
351
|
+
parent_obj.instance_variable_set('@'+obj_name, get_node_text(obj_node).to_f)
|
352
|
+
else
|
353
|
+
parent_obj.instance_variable_set('@'+obj_name, get_node_text(obj_node))
|
354
|
+
end
|
355
|
+
return obj_instance
|
356
|
+
else
|
357
|
+
# puts obj_name+" is a complex type "+obj_type+" in "+parent_obj.to_s
|
358
|
+
if !@object_map.has_key?(obj_type)
|
359
|
+
raise MarshallException.new("Unknown object: "+obj_type.to_s)
|
360
|
+
end
|
361
|
+
|
362
|
+
class_obj = @object_map[obj_type]
|
363
|
+
obj_instance = class_obj.new
|
364
|
+
parent_obj.instance_variable_set('@'+obj_name, obj_instance)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
# puts "Processing children of "+obj_node.name
|
368
|
+
children = obj_node.children
|
369
|
+
children.each do |child|
|
370
|
+
unmarshall_obj(child, parent_obj=obj_instance)
|
371
|
+
end
|
372
|
+
return obj_instance
|
373
|
+
end
|
374
|
+
|
375
|
+
private
|
376
|
+
def encode_primitive_xml(tag_name, value)
|
377
|
+
if value.is_a?(DateTime)
|
378
|
+
return ['<', tag_name, '>', value.strftime("%Y-%m-%dT%H:%M:%S%z"), '</', tag_name, '>'].join('')
|
379
|
+
else
|
380
|
+
['<', tag_name, '>', value.to_s, '</', tag_name, '>'].join('')
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
def list_to_xml(list_obj, list_type)
|
386
|
+
xml_data = Array.new
|
387
|
+
list_obj.each do |item|
|
388
|
+
if item.is_a?(DateTime) or item.is_a?(Integer) or item.is_a?(Float) or item.is_a?(String) or item.is_a?(TrueClass) or item.is_a?(FalseClass)
|
389
|
+
xml_data.push(encode_primitive_xml(list_type, item))
|
390
|
+
else
|
391
|
+
xml_data.push(['<', list_type, '>', encode_obj_xml(item), '</', list_type, '>'].join(''))
|
392
|
+
end
|
393
|
+
end
|
394
|
+
return xml_data.join('')
|
395
|
+
end
|
396
|
+
|
397
|
+
private
|
398
|
+
def encode_obj_xml(obj)
|
399
|
+
type_map_name = '_'+obj.class.name.split('::').last+'__type_map'
|
400
|
+
type_map = obj.instance_variable_get('@'+type_map_name)
|
401
|
+
xml_data = Array.new
|
402
|
+
|
403
|
+
obj.instance_variables.each do |var|
|
404
|
+
k = var.to_s.delete('@')
|
405
|
+
# puts "Var: "+var.to_s+" TMN: "+type_map_name.to_s
|
406
|
+
if k == type_map_name
|
407
|
+
next
|
408
|
+
end
|
409
|
+
v = obj.instance_variable_get(var)
|
410
|
+
data_type = type_map[k]
|
411
|
+
|
412
|
+
if data_type.is_a?(Array)
|
413
|
+
data_type = data_type[0]
|
414
|
+
end
|
415
|
+
|
416
|
+
# print "Encoding "+k.to_s+" "+v.to_s
|
417
|
+
key = k
|
418
|
+
if v.is_a?(Array)
|
419
|
+
xml_data.push(['<', key, '>', list_to_xml(v, data_type), '</', key, '>'].join(''))
|
420
|
+
elsif v.is_a?(DateTime)
|
421
|
+
xml_data.push(encode_primitive_xml(key, v))
|
422
|
+
elsif DATA_TYPES.include?(data_type)
|
423
|
+
xml_data.push(encode_primitive_xml(key, v))
|
424
|
+
else
|
425
|
+
xml_data.push(['<', key, '>', encode_obj_xml(v), '</', key, '>'].join(''))
|
426
|
+
end
|
427
|
+
end
|
428
|
+
return xml_data.join('')
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: DistelliServiceMarshallers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rahul Singh
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: DistelliServiceInterface
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: nokogiri
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Distelli Service Marshallers for Ruby Servers and Clients
|
47
|
+
email: rsingh@distelli.com
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- lib/distelli/servicemarshallers.rb
|
53
|
+
homepage: http://www.distelli.com/
|
54
|
+
licenses: []
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.8.23
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Distelli Service Marshaller classes
|
77
|
+
test_files: []
|