rubyosa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/src/lib/rbosa.rb ADDED
@@ -0,0 +1,761 @@
1
+ # Copyright (c) 2006, Apple Computer, Inc. All rights reserved.
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions
5
+ # are met:
6
+ # 1. Redistributions of source code must retain the above copyright
7
+ # notice, this list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright
9
+ # notice, this list of conditions and the following disclaimer in the
10
+ # documentation and/or other materials provided with the distribution.
11
+ # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
12
+ # its contributors may be used to endorse or promote products derived
13
+ # from this software without specific prior written permission.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND
16
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR
19
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21
+ # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
23
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
24
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ require 'osa'
28
+ require 'date'
29
+
30
+ # Try to load RubyGems first, libxml-ruby may have been installed by it.
31
+ begin require 'rubygems' rescue LoadError end
32
+
33
+ # If libxml-ruby is not present, switch to REXML.
34
+ USE_LIBXML = begin
35
+ require 'xml/libxml'
36
+
37
+ # libxml-ruby bug workaround.
38
+ class XML::Node
39
+ alias_method :old_cmp, :==
40
+ def ==(x)
41
+ (x != nil and old_cmp(x))
42
+ end
43
+ end
44
+ true
45
+ rescue LoadError
46
+ require 'rexml/document'
47
+
48
+ # REXML -> libxml-ruby compatibility layer.
49
+ class REXML::Element
50
+ alias_method :old_find, :find
51
+ def find(path=nil, &block)
52
+ if path.nil? and block
53
+ old_find { |*x| block.call(*x) }
54
+ else
55
+ list = []
56
+ ::REXML::XPath.each(self, path) { |e| list << e }
57
+ list
58
+ end
59
+ end
60
+ def [](attr)
61
+ attributes[attr]
62
+ end
63
+ def find_first(path)
64
+ ::REXML::XPath.first(self, path)
65
+ end
66
+ end
67
+ false
68
+ end
69
+
70
+ class String
71
+ def to_4cc
72
+ OSA.__four_char_code__(self)
73
+ end
74
+ end
75
+
76
+ class OSA::Enumerator
77
+ attr_reader :code, :name, :group_code
78
+
79
+ def initialize(const, name, code, group_code)
80
+ @const, @name, @code, @group_code = const, name, code, group_code
81
+ self.class.instances[code] = self
82
+ end
83
+
84
+ def self.enum_for_code(code)
85
+ instances[code]
86
+ end
87
+
88
+ def to_s
89
+ @name
90
+ end
91
+
92
+ def inspect
93
+ "<#{@const}>"
94
+ end
95
+
96
+ #######
97
+ private
98
+ #######
99
+
100
+ def self.instances
101
+ (@@instances rescue @@instances = {})
102
+ end
103
+ end
104
+
105
+ class OSA::Element
106
+ REAL_NAME = CODE = nil
107
+ def to_rbobj
108
+ unless __type__ == 'null'
109
+ val = OSA.convert_to_ruby(self)
110
+ val == nil ? self : val
111
+ end
112
+ end
113
+
114
+ def self.from_rbobj(requested_type, value, enum_group_codes)
115
+ self.__new__(*OSA.convert_to_osa(requested_type, value, enum_group_codes))
116
+ end
117
+ end
118
+
119
+ class OSA::ElementList
120
+ include Enumerable
121
+ def each
122
+ self.size.times { |i| yield(self[i]) }
123
+ end
124
+ end
125
+
126
+ class OSA::ElementRecord
127
+ def to_hash
128
+ h = {}
129
+ self.to_a.each { |key, val| h[key] = val.to_rbobj }
130
+ return h
131
+ end
132
+ end
133
+
134
+ module OSA::ObjectSpecifier
135
+ def get
136
+ @app.__send_event__('core', 'getd', [['----', self]], true).to_rbobj
137
+ end
138
+ end
139
+
140
+ class OSA::ObjectSpecifierList
141
+ include Enumerable
142
+
143
+ def initialize(app, desired_class, container)
144
+ @app, @desired_class, @container = app, desired_class, container
145
+ end
146
+
147
+ def length
148
+ @length ||= @app.__send_event__(
149
+ 'core', 'cnte',
150
+ [['----', @container], ['kocl', OSA::Element.__new__('type', @desired_class::CODE.to_4cc)]],
151
+ true).to_rbobj
152
+ end
153
+ alias_method :size, :length
154
+
155
+ def [](idx)
156
+ idx += 1 # AE starts counting at 1.
157
+ o = obj_spec_with_key(OSA::Element.__new__('long', [idx].pack('l')))
158
+ o.instance_variable_set(:@app, @app)
159
+ o.extend OSA::ObjectSpecifier
160
+ end
161
+
162
+ def each
163
+ self.length.times { |i| yield(self[i]) }
164
+ end
165
+
166
+ def get
167
+ o = obj_spec_with_key(OSA::Element.__new__('abso', 'all '.to_4cc))
168
+ o.instance_variable_set(:@app, @app)
169
+ o.extend OSA::ObjectSpecifier
170
+ o.get
171
+ end
172
+ alias_method :to_a, :get
173
+
174
+ def ==(other)
175
+ other.kind_of?(self.class) \
176
+ and other.length == self.length \
177
+ and (0..other.length).all? { |i| other[i] == self[i] }
178
+ end
179
+
180
+ #######
181
+ private
182
+ #######
183
+
184
+ def obj_spec_with_key(element)
185
+ @desired_class.__new_object_specifier__(@desired_class::CODE, @container,
186
+ 'indx', element)
187
+ end
188
+ end
189
+
190
+ module OSA
191
+ def self.app_with_name(name)
192
+ self.__app__(*OSA.__scripting_info__(:by_name, name))
193
+ end
194
+
195
+ def self.app_with_path(path)
196
+ self.__app__(*OSA.__scripting_info__(:by_path, path))
197
+ end
198
+
199
+ def self.app_with_bundle_id(bundle_id)
200
+ self.__app__(*OSA.__scripting_info__(:by_bundle_id, bundle_id))
201
+ end
202
+
203
+ def self.app_with_signature(signature)
204
+ self.__app__(*OSA.__scripting_info__(:by_signature, signature))
205
+ end
206
+
207
+ def self.app(name)
208
+ self.app_with_name(name)
209
+ end
210
+
211
+ @conversions_to_ruby = {}
212
+ @conversions_to_osa = {}
213
+
214
+ def self.add_conversion(hash, types, block, max_arity, replace=false)
215
+ raise "Conversion block has to accept either #{(1..max_arity).to_a.join(', ')} arguments" unless (1..max_arity) === block.arity
216
+ types.each do |type|
217
+ next if !replace and hash.has_key?(type)
218
+ hash[type] = block
219
+ end
220
+ end
221
+
222
+ def self.replace_conversion_to_ruby(*types, &block)
223
+ add_conversion(@conversions_to_ruby, types, block, 3, true)
224
+ end
225
+
226
+ def self.add_conversion_to_ruby(*types, &block)
227
+ add_conversion(@conversions_to_ruby, types, block, 3)
228
+ end
229
+
230
+ def self.replace_conversion_to_osa(*types, &block)
231
+ add_conversion(@conversions_to_osa, types, block, 2, true)
232
+ end
233
+
234
+ def self.add_conversion_to_osa(*types, &block)
235
+ add_conversion(@conversions_to_osa, types, block, 2)
236
+ end
237
+
238
+ def self.convert_to_ruby(osa_object)
239
+ osa_type = osa_object.__type__.to_s
240
+ osa_data = osa_object.__data__(osa_type) if osa_type and osa_type != 'null'
241
+ if conversion = @conversions_to_ruby[osa_type]
242
+ args = [osa_data, osa_type, osa_object]
243
+ conversion.call(*args[0..(conversion.arity - 1)])
244
+ end
245
+ end
246
+
247
+ def self.convert_to_osa(requested_type, value, enum_group_codes=nil)
248
+ if conversion = @conversions_to_osa[requested_type]
249
+ args = [value, requested_type]
250
+ conversion.call(*args[0..(conversion.arity - 1)])
251
+ elsif enum_group_codes and enum_group_codes.include?(requested_type)
252
+ ['enum', value.code.to_4cc]
253
+ else
254
+ STDERR.puts "unrecognized type #{requested_type}" if $VERBOSE
255
+ ['null', nil]
256
+ end
257
+ end
258
+
259
+ def self.set_params(hash)
260
+ previous_values = {}
261
+ hash.each do |key, val|
262
+ ivar_key = '@' + key.to_s
263
+ previous_val = self.instance_variable_get(ivar_key)
264
+ if previous_val.nil?
265
+ raise ArgumentError, "Invalid key value (no parameter named #{key} was found)"
266
+ end
267
+ previous_values[ivar_key] = previous_val;
268
+ self.instance_variable_set(ivar_key, hash[key])
269
+ end
270
+ if block_given?
271
+ yield
272
+ previous_values.each { |key, val| self.instance_variable_set(key, val) }
273
+ end
274
+ nil
275
+ end
276
+
277
+ #######
278
+ private
279
+ #######
280
+
281
+ class DocItem
282
+ attr_reader :name, :description
283
+ def initialize(name, description)
284
+ @name = name
285
+ @description = description
286
+ end
287
+ end
288
+
289
+ class DocMethod < DocItem
290
+ attr_reader :result, :args
291
+ def initialize(name, description, result, args)
292
+ super(name, description)
293
+ @result = result
294
+ @args = args
295
+ end
296
+ def inspect
297
+ "<Method #{name} (#{description})>"
298
+ end
299
+ end
300
+
301
+ def self.__app__(name, signature, sdef)
302
+ @apps ||= {}
303
+ app = @apps[signature]
304
+ return app if app
305
+ doc = if USE_LIBXML
306
+ parser = XML::Parser.new
307
+ parser.string = sdef
308
+ parser.parse
309
+ else
310
+ REXML::Document.new(sdef)
311
+ end
312
+
313
+ # Creates a module for this app, we will define the scripting interface within it.
314
+ app_module = Module.new
315
+ self.const_set(rubyfy_constant_string(name), app_module)
316
+
317
+ # Retrieves and creates enumerations.
318
+ enum_group_codes = {}
319
+ doc.find('/dictionary/suite/enumeration').each do |element|
320
+ enum_group_code = element['code']
321
+ enum_module_name = rubyfy_constant_string(element['name'], true)
322
+ enum_module = Module.new
323
+ enum_group_codes[enum_group_code] = enum_module
324
+
325
+ documentation = []
326
+ enum_module.const_set(:DESCRIPTION, documentation)
327
+
328
+ element.find('enumerator').each do |element|
329
+ name = element['name']
330
+ enum_name = rubyfy_constant_string(name, true)
331
+ enum_code = element['code']
332
+ enum_const = app_module.name + '::' + enum_module_name + '::' + enum_name
333
+
334
+ enum = OSA::Enumerator.new(enum_const, name, enum_code, enum_group_code)
335
+ enum_module.const_set(enum_name, enum)
336
+
337
+ documentation << DocItem.new(enum_name, englishify_sentence(element['description']))
338
+ end
339
+
340
+ app_module.const_set(enum_module_name, enum_module)
341
+ end
342
+
343
+ # Retrieves and creates classes.
344
+ classes = {}
345
+ class_elements = {}
346
+ doc.find('/dictionary/suite/class').each do |element|
347
+ (class_elements[element['name']] ||= []) << element
348
+ end
349
+ class_elements.values.flatten.each do |element|
350
+ klass = add_class_from_xml_element(element, class_elements, classes, app_module)
351
+ methods_doc = []
352
+ description = englishify_sentence(element['description'])
353
+ if klass.const_defined?(:DESCRIPTION)
354
+ klass.const_set(:DESCRIPTION, description) if klass.const_get(:DESCRIPTION).nil?
355
+ else
356
+ klass.const_set(:DESCRIPTION, description)
357
+ end
358
+ if klass.const_defined?(:METHODS_DESCRIPTION)
359
+ methods_doc = klass.const_get(:METHODS_DESCRIPTION)
360
+ else
361
+ methods_doc = []
362
+ klass.const_set(:METHODS_DESCRIPTION, methods_doc)
363
+ end
364
+
365
+ # Creates properties.
366
+ element.find('property').each do |pelement|
367
+ name = pelement['name']
368
+ code = pelement['code']
369
+ type = pelement['type']
370
+ access = pelement['access']
371
+ description = pelement['description']
372
+ setter = (access == nil or access.include?('w'))
373
+
374
+ if type == 'reference'
375
+ pklass = OSA::Element
376
+ else
377
+ pklass = classes[type]
378
+ if pklass.nil?
379
+ pklass_elements = class_elements[type]
380
+ unless pklass_elements.nil?
381
+ pklass = add_class_from_xml_element(pklass_elements.first, class_elements, classes, app_module)
382
+ end
383
+ end
384
+ end
385
+
386
+ # Implicit 'get' if the property class is primitive (not defined in the sdef),
387
+ # otherwise just return an object specifier.
388
+ method_name = rubyfy_method(name, klass, type)
389
+ method_code = if pklass.nil?
390
+ <<EOC
391
+ def #{method_name}
392
+ @app.__send_event__('core', 'getd',
393
+ [['----', Element.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
394
+ 'prop', Element.__new__('type', '#{code}'.to_4cc))]],
395
+ true).to_rbobj
396
+ end
397
+ EOC
398
+ else
399
+ <<EOC
400
+ def #{method_name}
401
+ o = #{pklass.name}.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
402
+ 'prop', Element.__new__('type', '#{code}'.to_4cc))
403
+ unless OSA.lazy_events?
404
+ @app.__send_event__('core', 'getd', [['----', o]], true).to_rbobj
405
+ else
406
+ o.instance_variable_set(:@app, @app)
407
+ o.extend(OSA::ObjectSpecifier)
408
+ end
409
+ end
410
+ EOC
411
+ end
412
+
413
+ klass.class_eval(method_code)
414
+ ptypedoc = if pklass.nil?
415
+ if mod = enum_group_codes[type]
416
+ "a #{mod} enumeration"
417
+ else
418
+ type
419
+ end
420
+ else
421
+ "a #{pklass} object"
422
+ end
423
+ description[0] = description[0].chr.downcase if description
424
+ methods_doc << DocMethod.new(method_name, englishify_sentence("Gets the #{name} property -- #{description}"), DocItem.new('result', englishify_sentence("the property value, as #{ptypedoc}")), nil)
425
+
426
+ # For the setter, always send an event.
427
+ if setter
428
+ method_name = rubyfy_method(name, klass, type, true)
429
+ method_code = <<EOC
430
+ def #{method_name}(val)
431
+ @app.__send_event__('core', 'setd',
432
+ [['----', Element.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
433
+ 'prop', Element.__new__('type', '#{code}'.to_4cc))],
434
+ ['data', #{new_element_code(type, 'val', enum_group_codes)}]],
435
+ true)
436
+ return nil
437
+ end
438
+ EOC
439
+
440
+ klass.class_eval(method_code)
441
+ methods_doc << DocMethod.new(method_name, englishify_sentence("Sets the #{name} property -- #{description}"), nil, [DocItem.new('val', englishify_sentence("the value to be set, as #{ptypedoc}"))])
442
+ end
443
+ end
444
+
445
+ # Creates elements.
446
+ element.find('element').each do |eelement|
447
+ type = eelement['type']
448
+
449
+ eklass = classes[type]
450
+ if eklass.nil?
451
+ eklass_elements = class_elements[type]
452
+ unless eklass_elements.nil?
453
+ eklass = add_class_from_xml_element(eklass_elements.first, class_elements, classes, app_module)
454
+ end
455
+ end
456
+
457
+ if eklass.nil?
458
+ STDERR.puts "Cannot find class '#{type}', skipping element '#{eelement}'" if $DEBUG
459
+ next
460
+ end
461
+
462
+ method_name = rubyfy_method(eklass::PLURAL, klass)
463
+ method_code = <<EOC
464
+ def #{method_name}
465
+ unless OSA.lazy_events?
466
+ @app.__send_event__('core', 'getd',
467
+ [['----', Element.__new_object_specifier__(
468
+ '#{eklass::CODE}', @app == self ? Element.__new__('null', nil) : self,
469
+ 'indx', Element.__new__('abso', 'all '.to_4cc))]],
470
+ true).to_rbobj
471
+ else
472
+ ObjectSpecifierList.new(@app, #{eklass}, @app == self ? Element.__new__('null', nil) : self)
473
+ end
474
+ end
475
+ EOC
476
+
477
+ klass.class_eval(method_code)
478
+ methods_doc << DocMethod.new(method_name, englishify_sentence("Gets the #{eklass::PLURAL} associated with this object"), DocItem.new('result', englishify_sentence("an Array of #{eklass} objects")), nil)
479
+ end
480
+ end
481
+
482
+ # Having an 'application' class is required.
483
+ app_class = classes['application']
484
+ raise "No application class defined." if app_class.nil?
485
+
486
+ # Maps commands to the right classes.
487
+ all_classes_but_app = classes.values.reject { |x| x.ancestors.include?(OSA::EventDispatcher) }
488
+ doc.find('/dictionary/suite/command').each do |element|
489
+ name = element['name']
490
+ next if /NOT AVAILABLE/.match(name) # Finder's sdef (Tiger) names some commands with this 'tag'.
491
+ description = element['description']
492
+ code = element['code']
493
+ direct_parameter = element.find_first('direct-parameter')
494
+ result = element.find_first('result')
495
+
496
+ classes_to_define = []
497
+ forget_direct_parameter = true
498
+ direct_parameter_optional = false
499
+
500
+ if direct_parameter.nil?
501
+ # No direct parameter, this is for the application class.
502
+ classes_to_define << app_class
503
+ else
504
+ # We have a direct parameter:
505
+ # - map it to the right class if it's a class defined in our scripting dictionary
506
+ # - map it to all classes if it's a 'reference' and to the application class if it's optional
507
+ # - otherwise, just map it to the application class.
508
+ type = type_of_parameter(direct_parameter)
509
+ direct_parameter_optional = parameter_optional?(direct_parameter)
510
+
511
+ if type == 'reference'
512
+ classes_to_define = all_classes_but_app
513
+ classes_to_define << app_class if direct_parameter_optional
514
+ else
515
+ klass = classes[type]
516
+ if klass.nil?
517
+ forget_direct_parameter = false
518
+ classes_to_define << app_class
519
+ else
520
+ classes_to_define << klass
521
+ end
522
+ end
523
+ end
524
+
525
+ # Reject classes which are already represented by an ancestor.
526
+ if classes_to_define.length > 1
527
+ classes_to_define.uniq!
528
+ classes_to_define.reject! do |x|
529
+ classes_to_define.any? { |y| x != y and x.ancestors.include?(y) }
530
+ end
531
+ end
532
+
533
+ params = []
534
+ unless direct_parameter.nil?
535
+ params << ['direct',
536
+ '----',
537
+ direct_parameter['description'],
538
+ direct_parameter_optional,
539
+ type_of_parameter(direct_parameter)]
540
+ end
541
+
542
+ params.concat(element.find('parameter').to_a.map do |element|
543
+ [rubyfy_string(element['name'], true),
544
+ element['code'],
545
+ element['description'],
546
+ parameter_optional?(element),
547
+ type_of_parameter(element)]
548
+ end)
549
+
550
+ p_dec, p_def, p_args_doc = [], [], []
551
+ already_has_optional_args = false # Once an argument is optional, all following arguments should be optional.
552
+ params.each do |pname, pcode, pdesc, optional, ptype|
553
+ decl = pname
554
+ self_direct = (pcode == '----' and forget_direct_parameter)
555
+ defi = if self_direct
556
+ forget_direct_parameter ? "(self.is_a?(OSA::EventDispatcher) ? [] : ['----', self])" : "['----', self]"
557
+ else
558
+ "['#{pcode}', #{new_element_code(ptype, pname, enum_group_codes)}]"
559
+ end
560
+ if already_has_optional_args or (optional and !self_direct)
561
+ decl += '=nil'
562
+ defi = "(#{pname} == nil ? [] : #{defi})"
563
+ already_has_optional_args = true
564
+ end
565
+ unless self_direct
566
+ p_dec << decl
567
+ p_args_doc << DocItem.new(decl, englishify_sentence(pdesc))
568
+ end
569
+ p_def << defi
570
+ end
571
+
572
+ method_code = <<EOC
573
+ def %METHOD_NAME%(#{p_dec.join(', ')})
574
+ @app.__send_event__('#{code[0..3]}', '#{code[4..-1]}', [#{p_def.join(', ')}], #{result != nil})#{result != nil ? '.to_rbobj' : ''}
575
+ end
576
+ EOC
577
+
578
+ if result.nil?
579
+ result_type = result_doc = nil
580
+ else
581
+ result_type = type_of_parameter(result)
582
+ result_klass = classes[result_type]
583
+ result_doc = DocItem.new('result', englishify_sentence(result['description']))
584
+ end
585
+
586
+ classes_to_define.each do |klass|
587
+ method_name = rubyfy_method(name, klass, result_type)
588
+ code = method_code.sub(/%METHOD_NAME%/, method_name)
589
+ klass.class_eval(code)
590
+ methods_doc = klass.const_get(:METHODS_DESCRIPTION)
591
+ methods_doc << DocMethod.new(method_name, englishify_sentence(description), result_doc, p_args_doc)
592
+ end
593
+ end
594
+
595
+ # Returns an application instance, that's all folks!
596
+ hash = {}
597
+ classes.each_value { |klass| hash[klass::CODE] = klass }
598
+ app = app_class.__new__('sign', signature.to_4cc)
599
+ app.instance_variable_set(:@sdef, sdef)
600
+ app.instance_variable_set(:@classes, hash)
601
+ app.instance_eval 'def sdef; @sdef; end'
602
+ app.extend OSA::EventDispatcher
603
+ @apps[signature] = app
604
+ end
605
+
606
+ def self.parameter_optional?(element)
607
+ element['optional'] == 'yes'
608
+ end
609
+
610
+ def self.add_class_from_xml_element(element, class_elements, repository, app_module)
611
+ real_name = element['name']
612
+ klass = repository[real_name]
613
+ if klass.nil?
614
+ code = element['code']
615
+ inherits = element['inherits']
616
+ plural = element['plural']
617
+
618
+ if real_name == inherits
619
+ # Inheriting from itself is a common idiom when adding methods
620
+ # to a class that has already been defined, probably to avoid
621
+ # mentioning the subclass name more than once.
622
+ inherits = nil
623
+ end
624
+
625
+ if inherits.nil?
626
+ klass = Class.new(OSA::Element)
627
+ else
628
+ super_elements = class_elements[inherits]
629
+ if super_elements.nil?
630
+ STDERR.puts "sdef bug: class '#{real_name}' inherits from '#{inherits}' which is not defined - fall back inheriting from OSA::Element" if $DEBUG
631
+ klass = OSA::Element
632
+ else
633
+ super_class = add_class_from_xml_element(super_elements.first, class_elements,
634
+ repository, app_module)
635
+ klass = Class.new(super_class)
636
+ end
637
+ end
638
+
639
+ klass.class_eval 'include OSA::EventDispatcher' if real_name == 'application'
640
+
641
+ klass.const_set(:REAL_NAME, real_name) unless klass.const_defined?(:REAL_NAME)
642
+ klass.const_set(:PLURAL, plural == nil ? real_name + 's' : plural) unless klass.const_defined?(:PLURAL)
643
+ klass.const_set(:CODE, code) unless klass.const_defined?(:CODE)
644
+
645
+ app_module.const_set(rubyfy_constant_string(real_name), klass)
646
+
647
+ repository[real_name] = klass
648
+ end
649
+
650
+ return klass
651
+ end
652
+
653
+ def self.type_of_parameter(element)
654
+ type = element['type']
655
+ if type.nil?
656
+ etype = element.find_first('type')
657
+ if etype.nil? or (type = etype['type']).nil?
658
+ raise "Parameter #{element} has no type."
659
+ end
660
+ type = "list_of_#{type}" if etype['list'] == 'yes'
661
+ end
662
+ return type
663
+ end
664
+
665
+ def self.new_element_code(type, varname, enum_group_codes)
666
+ if md = /^list_of_(.+)$/.match(type)
667
+ return "#{varname}.is_a?(OSA::Element) ? #{varname} : ElementList.__new__(#{varname}.to_a.map { |x| #{new_element_code(md[1], 'x', enum_group_codes)} })"
668
+ end
669
+ "#{varname}.is_a?(OSA::Element) ? #{varname} : Element.from_rbobj('#{type}', #{varname}, #{enum_group_codes.inspect})"
670
+ end
671
+
672
+ def self.escape_string(string)
673
+ string.gsub(/[\s\-\.\/]/, '_').gsub(/&/, 'and')
674
+ end
675
+
676
+ def self.rubyfy_constant_string(string, upcase=false)
677
+ if /^\d/.match(string)
678
+ string = 'C' << string
679
+ else
680
+ string = string.dup
681
+ string[0] = string[0].chr.upcase
682
+ end
683
+ escape_string(upcase ? string.upcase : string.gsub(/\s(.)/) { |s| s[1].chr.upcase })
684
+ end
685
+
686
+ RUBY_RESERVED_KEYWORDS = ['for', 'in']
687
+ def self.rubyfy_string(string, handle_ruby_reserved_keywords=false)
688
+ # Prefix with '_' parameter names to avoid possible collisions with reserved Ruby keywords (for, etc...).
689
+ if RUBY_RESERVED_KEYWORDS.include?(string)
690
+ '_' + string
691
+ else
692
+ escape_string(string).downcase
693
+ end
694
+ end
695
+
696
+ def self.rubyfy_method(string, klass, return_type=nil, setter=false)
697
+ s = rubyfy_string(string)
698
+ if setter
699
+ # Suffix setters with '='.
700
+ s << '='
701
+ elsif return_type == 'boolean'
702
+ # Suffix predicates with '?'.
703
+ s << '?'
704
+ end
705
+ # Prefix with 'osa_' in case the class already has a method with such a name.
706
+ if klass.method_defined?(s)
707
+ s = 'osa_' + s
708
+ end
709
+ return s
710
+ end
711
+
712
+ def self.englishify_sentence(string)
713
+ return '' if string.nil?
714
+ string[0] = string[0].chr.upcase
715
+ last = string[-1].chr
716
+ string << '.' if last != '.' and last != '?' and last != '!'
717
+ return string
718
+ end
719
+ end
720
+
721
+ # String, force TEXT type to not get Unicode.
722
+ OSA.add_conversion_to_ruby('TEXT', 'utxt') { |value, type, object| object.__data__('TEXT') }
723
+ OSA.add_conversion_to_osa('string', 'text', 'Unicode text') { |value| ['TEXT', value.to_s] }
724
+
725
+ # Signed/unsigned integer.
726
+ OSA.add_conversion_to_ruby('shor', 'long', 'comp') { |value| value.unpack('l').first }
727
+ OSA.add_conversion_to_ruby('magn') { |value| value.unpack('d').first }
728
+ OSA.add_conversion_to_osa('integer', 'double integer') { |value| ['magn', [value].pack('l')] }
729
+
730
+ # Boolean.
731
+ OSA.add_conversion_to_ruby('bool') { |value| value.unpack('c').first != 0 }
732
+ OSA.add_conversion_to_osa('boolean') { |value| [(value ? 'true'.to_4cc : 'fals'.to_4cc), nil] }
733
+ OSA.add_conversion_to_ruby('true') { |value| true }
734
+ OSA.add_conversion_to_ruby('fals') { |value| false }
735
+
736
+ # Date.
737
+ OSA.add_conversion_to_ruby('ldt ') { |value|
738
+ Date.new(1904, 1, 1) + Date.time_to_day_fraction(0, 0, value.unpack('q').first)
739
+ }
740
+
741
+ # Array.
742
+ OSA.add_conversion_to_ruby('list') { |value, type, object|
743
+ object.is_a?(OSA::ElementList) ? object.to_a.map { |x| x.to_rbobj } : object
744
+ }
745
+
746
+ # File name.
747
+ # Let's use the 'furl' type here instead of 'alis', as we don't have a way to produce an alias for a file that does not exist yet.
748
+ OSA.add_conversion_to_osa('alias', 'file') { |value| ['furl', value.to_s] }
749
+
750
+ # Hash.
751
+ OSA.add_conversion_to_ruby('reco') { |value, type, object| object.is_a?(OSA::ElementRecord) ? object.to_hash : self }
752
+
753
+ # Enumerator.
754
+ OSA.add_conversion_to_ruby('enum') { |value, type, object| OSA::Enumerator.enum_for_code(object.__data__('TEXT')) or self }
755
+
756
+ # Class.
757
+ OSA.add_conversion_to_osa('type class') { |value| value.is_a?(Class) and value.ancestors.include?(OSA::Element) ? ['type', value::CODE.to_4cc] : self }
758
+
759
+ # QuickDraw Rectangle, aka "bounding rectangle".
760
+ OSA.add_conversion_to_ruby('qdrt') { |value| value.unpack('S4') }
761
+ OSA.add_conversion_to_osa('bounding rectangle') { |value| ['qdrt', value.pack('S4')] }