rubyosa 0.1.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/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')] }