ginjo-rfm 2.1.7 → 3.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.
@@ -0,0 +1,1039 @@
1
+ # #### A declarative SAX parser, written by William Richardson #####
2
+ #
3
+ # This XML parser builds a result object from callbacks sent by any ruby sax/stream parsing
4
+ # engine. The engine can be any ruby xml parser that offers a sax or streaming parsing scheme
5
+ # that sends callbacks to a handler object for the various node events encountered in the xml stream.
6
+ # The currently supported parsers are the Ruby librarys libxml-ruby, nokogiri, ox, and rexml.
7
+ #
8
+ # Without a configuration template, this parser will return a generic tree of hashes and arrays,
9
+ # representing the xml structure & data that was fed into the parser. With a configuration template,
10
+ # this parser can create resulting objects that are custom transformations of the input xml.
11
+ #
12
+ # The goal in writing this parser was to build custom objects from xml, in a single pass,
13
+ # without having to build a generic tree first and then pick it apart with ugly code scattered all over
14
+ # our projects' classes.
15
+ #
16
+ # The primaray use case that motivated this parser's construction was converting Filemaker Server's
17
+ # xml response documents into Ruby result-set arrays containing record hashes. A primary example of
18
+ # this use can be seen in the Ruby gem 'ginjo-rfm' (a Ruby-Filemaker adapter).
19
+ #
20
+ #
21
+ # Useage:
22
+ # irb -rubygems -I./ -r lib/rfm/utilities/sax_parser.rb
23
+ # SaxParser.parse(io, template=nil, initial_object=nil, parser=nil, options={})
24
+ # io: xml-string or xml-file-path or file-io or string-io
25
+ # template: file-name, yaml, xml, symbol, or hash
26
+ # initial_object: the parent object - any object to which the resulting build will be attached to.
27
+ # parser: backend parser symbol or custom backend handler instance
28
+ # options: extra options
29
+ #
30
+ #
31
+ # Note: 'attach: cursor' puts the object in the cursor & stack but does not attach it to the parent.
32
+ # 'attach: none' prevents the object from entering the cursor or stack.
33
+ # Both of these will still allow processing of attributes and subelements.
34
+ #
35
+ # Examples:
36
+ # Rfm::SaxParser.parse('some/file.xml') # => defaults to best xml backend with no parsing configuration.
37
+ # Rfm::SaxParser.parse('some/file.xml', nil, nil, :ox) # => uses ox backend or throws error.
38
+ # Rfm::SaxParser.parse('some/file.xml', {'compact'=>true}, :rexml) # => uses inline configuration with rexml parser.
39
+ # Rfm::SaxParser.parse('some/file.xml', 'path/to/config.yml', SomeClass.new) # => loads config template from yml file and builds on top of instance of SomeClass.
40
+ #
41
+ #
42
+ # #### CONFIGURATION #####
43
+ #
44
+ # YAML structure defining a SAX xml parsing template.
45
+ # Options:
46
+ # initialize: array: initialize new objects with this code [:method, params] instead of defaulting to 'allocate'
47
+ # elements: array of element hashes [{'name'=>'element-tag'},...]
48
+ # attributes: array of attribute hashes {'name'=>'attribute-name'} UC
49
+ # class: NOT USED string-or-class: class name for new element
50
+ # depth: integer: depth-of-default-class UC
51
+ # attach: string: shared, _shared_name, private, hash, array, cursor, none - attach this element or attribute to parent.
52
+ # attach_elements: string: same as 'attach' - for all subelements, unless they have their own 'attach' specification
53
+ # attach_attributes: string: same as 'attach' - for all attributes, unless they have their own 'attach' specification
54
+ # before_close: symbol (method) or string (code): run a model method before closing tag, passing in #cursor. String is eval'd in context of object.
55
+ # as_name: string: store element or attribute keyed as specified
56
+ # delimiter: string: attribute/hash key to delineate objects with identical tags
57
+ # create_accessors: UC string or array: all, private, shared, hash, none
58
+ # accessor: UC string: all, private, shared, hash, none
59
+ # handler: array: call an object with any params [obj, method, params]. Default attach prefs are 'cursor'.
60
+ #
61
+ #
62
+ # #### See below for notes & todos ####
63
+
64
+
65
+ require 'yaml'
66
+ require 'forwardable'
67
+ require 'stringio'
68
+
69
+ module Rfm
70
+ module SaxParser
71
+ extend Forwardable
72
+
73
+ PARSERS = {}
74
+ # OPTIONS constant not yet used.
75
+ # OPTIONS = [:name, :elements, :attributes, :attach, :attach_elements, :attach_attributes, :compact,
76
+ # :depth, :before_close, :each_before_close, :delimiter, :as_name, :initialize, :handler
77
+ # ]
78
+ DEFAULTS = [:default_class, :backend, :text_label, :tag_translation, :shared_instance_var, :templates, :template_prefix]
79
+
80
+
81
+ class << self
82
+ attr_accessor *DEFAULTS
83
+ end
84
+
85
+ ### Default class MUST be a descendant of Hash or respond to hash methods !!!
86
+ (@default_class = Hash) unless defined? @default_class
87
+
88
+ # Use :libxml, :nokogiri, :ox, :rexml, or anything else, if you want it to always default
89
+ # to something other than the fastest backend found.
90
+ # Nil will let the SaxParser decide.
91
+ (@backend = nil) unless defined? @backend
92
+
93
+ (@text_label = 'text') unless defined? @text_label
94
+
95
+ (@tag_translation = lambda {|txt| txt.gsub(/\-/, '_').downcase}) unless defined? @tag_translation
96
+
97
+ (@shared_instance_var = 'attributes') unless defined? @shared_instance_var
98
+
99
+ (@templates = {}) unless defined? @templates
100
+
101
+ def self.parse(*args)
102
+ Handler.build(*args)
103
+ end
104
+
105
+ # Installs attribute accessors for defaults
106
+ def self.install_defaults(klass)
107
+ klass.extend Forwardable
108
+ klass.def_delegators SaxParser, *DEFAULTS
109
+ class << klass
110
+ extend Forwardable
111
+ def_delegators SaxParser, *DEFAULTS
112
+ end
113
+ end
114
+
115
+
116
+ # A Cursor instance is created for each element encountered in the parsing run
117
+ # and is where the parsing result is constructed from the custom parsing template.
118
+ # The cursor is the glue between the handler and the resulting object build. The
119
+ # cursor receives input from the handler, loads the corresponding template data,
120
+ # and manipulates the incoming xml data to build the resulting object.
121
+ #
122
+ # Each cursor is added to the stack when its element begins, and removed from
123
+ # the stack when its element ends. The cursor tracks all the important objects
124
+ # necessary to build the resulting object. If you call #cursor on the handler,
125
+ # you will always get the last object added to the stack. Think of a cursor as
126
+ # a framework of tools that accompany each element's build process.
127
+ class Cursor
128
+
129
+ #attr_accessor :model, :object, :tag, :parent, :top, :stack, :newtag, :callbacks
130
+ attr_accessor :model, :object, :tag, :newtag, :callbacks, :handler, :parent #, :top, :stack,
131
+
132
+ SaxParser.install_defaults(self)
133
+
134
+ def_delegators :handler, :top, :stack
135
+
136
+ # Main get-constant method
137
+ def self.get_constant(klass)
138
+ #puts "Getting constant '#{klass.to_s}'"
139
+
140
+ case
141
+ when klass.is_a?(Class); klass
142
+ when (klass=klass.to_s) == ''; default_class
143
+ when klass[/::/]; eval(klass)
144
+ when defined?(klass); const_get(klass) ## == 'constant'; const_get(klass)
145
+ #when defined?(klass); eval(klass) # This was for 'handler' pattern.
146
+ else
147
+ Rfm.log.warn "Could not find constant '#{klass}'"
148
+ default_class
149
+ end
150
+
151
+ end
152
+
153
+ def initialize(_model, _obj, _tag, _handler)
154
+ @tag = _tag
155
+ @model = _model
156
+ @object = _obj #_obj.is_a?(String) ? get_constant(_obj).new : _obj
157
+ @callbacks = {}
158
+ @handler = _handler
159
+ self
160
+ end
161
+
162
+ def handler
163
+ @handler
164
+ end
165
+
166
+
167
+
168
+ ##### SAX METHODS #####
169
+
170
+ # Not sure if this is still used.
171
+ def receive_attribute(name, value)
172
+ #puts "\nRECEIVE_ATTR '#{name}' value '#{value}' object '#{object.class}' model '#{model.keys}' subm '#{submodel.keys}' tag '#{tag}' newtag '#{newtag}'"
173
+ new_att = default_class.new.tap{|att| att[name]=value}
174
+ assign_attributes(new_att, object, model, model)
175
+ rescue
176
+ Rfm.log.warn "Error: could not assign attribute '#{name.to_s}' to element '#{self.tag.to_s}': #{$!}"
177
+ end
178
+
179
+ def receive_start_element(_tag, _attributes)
180
+ # TODO: Use a case statement to separate the various tasks possible in this method
181
+
182
+
183
+ #puts ['START', _tag, object.class, model]
184
+ # Set newtag for other methods to use during the start_el run.
185
+ @newtag = _tag
186
+
187
+ # Acquire submodel definition.
188
+ subm = model_elements?(@newtag) || default_class.new
189
+
190
+ # Get attachment_prefs
191
+ prefs = attachment_prefs(model, subm, 'element')
192
+
193
+ #puts "RECEIVE_START_ELEMENT: _tag '#{_tag}'\ncurrent object '#{object.class}'\ncursor_model '#{model}'\ncursor_submodel '#{subm.to_yaml}', attributes '#{_attributes.to_a}'"
194
+
195
+ # Clean-up and return if new element is not to be attached.
196
+ if prefs == 'none'
197
+ # Set callbacks, since object & model of new element won't be stored.
198
+ callbacks[_tag] = lambda {run_callback(_tag, self, subm)}
199
+ #puts "Assigning attributes for attach:none on #{newtag}"
200
+ # This passes current model, since that is the model that will be accepting thiese attributes, if any.
201
+ assign_attributes(_attributes, object, model, subm)
202
+ return
203
+ end
204
+
205
+ if handler?(subm)
206
+ code = handler?(subm)
207
+ obj = eval(code[0].to_s)
208
+ mthd = code[1].to_s
209
+ prms = eval(code[2].to_s)
210
+ new_element = obj.send(mthd, prms)
211
+ #puts ["\nIF_HANDLER", code, new_element.class, new_element]
212
+ else
213
+ # Create new element.
214
+ const = get_constant(subm['class'])
215
+ # Needs to be duped !!!
216
+ init = initialize?(subm) ? initialize?(subm).dup : []
217
+ #puts init.to_yaml
218
+ init[0] ||= :allocate
219
+ init[1] = eval(init[1].to_s)
220
+ #puts "Current object: #{eval('object').class}"
221
+ #puts "Creating new element '#{const}' with '#{init[0]}' and '#{init[1].class}'"
222
+ new_element = const.send(*init.compact)
223
+ #puts "Created new element of class '#{new_element.class}' for _tag '#{tag}'."
224
+
225
+ # Assign attributes to new element.
226
+ assign_attributes(_attributes, new_element, subm, subm) #unless attach_attributes?(subm) == 'none'
227
+
228
+ # Attach new element to cursor object
229
+ attach_new_object(object, new_element, newtag, model, subm, 'element') #unless prefs == 'cursor'
230
+ end
231
+
232
+ returntag = newtag
233
+ self.newtag = nil
234
+ Cursor.new(subm, new_element, returntag, handler)
235
+ end # start_el
236
+
237
+ def receive_end_element(_tag)
238
+ #puts ["\nEND_ELEMENT #{_tag}", "CurrentObject: #{object.class}", "CurrentTag: #{self.tag}", "CurrentModel: #{model}", "BeforeClose: #{before_close?}", "Compact: #{compact?}"]
239
+ begin
240
+ if _tag == self.tag
241
+ # Data cleaup
242
+ compactor_settings = compact?
243
+ (compactor_settings = compact?(top.model)) unless compactor_settings # prefer local settings, or use top settings.
244
+ (clean_members {|v| clean_members(v){|v| clean_members(v)}}) if compactor_settings
245
+ end
246
+
247
+ # # EXPERIMENTAL: sending modle PLUS submodel prefs to _create_accessors
248
+ # # Acquire submodel definition for create accessors (EXPERIMENTAL)
249
+ # subm = model_elements?(_tag) || default_class.new
250
+ # accessor_options = (create_accessors? | create_accessors?(subm))
251
+ # if accessor_options.any?
252
+ # #puts ["CREATING_ACCESSORS #{accessor_options}"]
253
+ # object._create_accessors(accessor_options)
254
+ # end
255
+
256
+ # # Create accessors is specified.
257
+ # # TODO: This creates redundant calls when elements close with the same model as current. But how to get the correct model when elements close that are attach:none ???
258
+ # if create_accessors?.any?
259
+ # #puts ['CREATING_ACCESSORS', create_accessors?]
260
+ # object._create_accessors(create_accessors?)
261
+ # end
262
+
263
+ # Run callback of non-stored element.
264
+ callbacks[_tag].call if callbacks[_tag]
265
+ if _tag == self.tag
266
+ # End-element callbacks.
267
+ run_callback(_tag, self)
268
+ # if before_close?.is_a? Symbol
269
+ # object.send before_close?, self
270
+ # elsif before_close?.is_a?(String)
271
+ # object.send :eval, before_close?
272
+ # end
273
+ end
274
+
275
+ # return true only if matching tags
276
+ if _tag == self.tag
277
+ return true
278
+ end
279
+ # rescue
280
+ # Rfm.log.warn "Error: end_element tag '#{_tag}' failed: #{$!}"
281
+ end
282
+ end
283
+
284
+ def run_callback(_tag, _cursor=self, _model=_cursor.model, _object=_cursor.object )
285
+ callback = before_close?(_model)
286
+ #puts ["\nRUN_CALLBACK", _tag, _cursor.tag, _object.class, callback]
287
+ if callback.is_a? Symbol
288
+ _object.send callback, _cursor
289
+ elsif callback.is_a?(String)
290
+ _object.send :eval, callback
291
+ end
292
+ end
293
+
294
+
295
+
296
+
297
+ ##### MERGE METHODS #####
298
+
299
+ # Assign attributes to element.
300
+ def assign_attributes(_attributes, base_object, base_model, new_model)
301
+ if _attributes && !_attributes.empty?
302
+
303
+ #puts ["\nASSIGN_ATTRIBUTES", base_object.class, base_model, new_model].join(' **** ')
304
+
305
+ # # This is trying to merge element set as a whole, if possible.
306
+ # # Experimental #
307
+ # #
308
+ # # OLD: prefs_exist = model_attributes?(nil, new_model) || attach_attributes?(base_model) || delimiter?(base_model)
309
+ # attribute_prefs = attach_attributes?(base_model)
310
+ # case
311
+ # when !model_attributes?(nil, base_model) && attribute_prefs.to_s[/shared|_/]
312
+ # shared_var_name = shared_variable_name(attribute_prefs) || 'attributes'
313
+ # #puts ["MASS", attribute_prefs, shared_var_name, _attributes.keys].join(', ')
314
+ # #attach_new_object(base_object, _attributes, shared_var_name, base_model, new_model, 'attribute')
315
+ # #base_object._attach_object!(_attributes, shared_var_name, nil, 'private', 'attribute', :default_class=>default_class, :create_accessors=>create_accessors?(base_model))
316
+ # (var = ivg(shared_var_name, base_object)) ? var.merge!(_attributes) : ivs(shared_var_name, _attributes, base_object)
317
+ # else
318
+ # #puts "Loading attributes individually."
319
+ # #puts ["INDIVIDUAL", attribute_prefs, _attributes.keys].join(', ')
320
+ _attributes.each{|k,v| attach_new_object(base_object, v, k, base_model, model_attributes?(k, new_model), 'attribute')}
321
+ # end
322
+ end
323
+ end
324
+
325
+ def attach_new_object(base_object, new_object, name, base_model, new_model, type)
326
+ label = label_or_tag(name, new_model)
327
+
328
+ prefs = attachment_prefs(base_model, new_model, type)
329
+
330
+ # shared_var_name = nil
331
+ # if prefs.to_s[0,1] == "_"
332
+ # shared_var_name = prefs.to_s[1,16]
333
+ # prefs = "shared"
334
+ # end
335
+ shared_var_name = shared_variable_name prefs
336
+ (prefs = "shared") if shared_var_name
337
+
338
+ # Use local create_accessors prefs first, then more general ones.
339
+ create_accessors = accessor?(new_model)
340
+ (create_accessors = create_accessors?(base_model)) unless create_accessors && create_accessors.any?
341
+
342
+
343
+ #puts ["\nCURSOR.attach_new_object 1", type, label, base_object.class, new_object.class, delimiter?(new_model), prefs, shared_var_name, create_accessors].join(', ')
344
+ base_object._attach_object!(new_object, label, delimiter?(new_model), prefs, type, :default_class=>default_class, :shared_variable_name=>shared_var_name, :create_accessors=>create_accessors)
345
+ #puts ["\nCURSOR.attach_new_object 2: #{base_object.class} with ", label, delimiter?(new_model), prefs, type, :default_class=>default_class, :shared_variable_name=>shared_var_name, :create_accessors=>create_accessors]
346
+ end
347
+
348
+ def attachment_prefs(base_model, new_model, type)
349
+ case type
350
+ when 'element'; attach?(new_model) || attach_elements?(base_model) #|| attach?(top.model) || attach_elements?(top.model)
351
+ when 'attribute'; attach?(new_model) || attach_attributes?(base_model) #|| attach?(top.model) || attach_attributes?(top.model)
352
+ end
353
+ end
354
+
355
+ def shared_variable_name(prefs)
356
+ rslt = nil
357
+ if prefs.to_s[0,1] == "_"
358
+ rslt = prefs.to_s[1,16] #prefs.gsub(/^_/, '')
359
+ end
360
+ end
361
+
362
+
363
+ ##### UTILITY #####
364
+
365
+ def get_constant(klass)
366
+ self.class.get_constant(klass)
367
+ end
368
+
369
+ # Methods for current _model
370
+ def ivg(name, _object=object); _object.instance_variable_get "@#{name}"; end
371
+ def ivs(name, value, _object=object); _object.instance_variable_set "@#{name}", value; end
372
+ def model_elements?(which=nil, _model=model); _model && _model.has_key?('elements') && ((_model['elements'] && which) ? _model['elements'].find{|e| e['name']==which} : _model['elements']) ; end
373
+ def model_attributes?(which=nil, _model=model); _model && _model.has_key?('attributes') && ((_model['attributes'] && which) ? _model['attributes'].find{|a| a['name']==which} : _model['attributes']) ; end
374
+ def depth?(_model=model); _model && _model['depth']; end
375
+ def before_close?(_model=model); _model && _model['before_close']; end
376
+ def each_before_close?(_model=model); _model && _model['each_before_close']; end
377
+ def compact?(_model=model); _model && _model['compact']; end
378
+ def attach?(_model=model); _model && _model['attach']; end
379
+ def attach_elements?(_model=model); _model && _model['attach_elements']; end
380
+ def attach_attributes?(_model=model); _model && _model['attach_attributes']; end
381
+ def delimiter?(_model=model); _model && _model['delimiter']; end
382
+ def as_name?(_model=model); _model && _model['as_name']; end
383
+ def initialize?(_model=model); _model && _model['initialize']; end
384
+ def create_accessors?(_model=model); _model && [_model['create_accessors']].flatten.compact; end
385
+ def accessor?(_model=model); _model && [_model['accessor']].flatten.compact; end
386
+ def handler?(_model=model); _model && _model['handler']; end
387
+
388
+
389
+ # Methods for submodel
390
+ def label_or_tag(_tag=newtag, new_model=submodel); as_name?(new_model) || _tag; end
391
+
392
+
393
+ def clean_members(obj=object)
394
+ #puts ["CURSOR.clean_members: #{object.class}", "tag: #{tag}", "model-name: #{model[:name]}"]
395
+ # cursor.object = clean_member(cursor.object)
396
+ # clean_members(ivg(shared_attribute_var, obj))
397
+ if obj.is_a?(Hash)
398
+ obj.dup.each do |k,v|
399
+ obj[k] = clean_member(v)
400
+ yield(v) if block_given?
401
+ end
402
+ elsif obj.is_a?(Array)
403
+ obj.dup.each_with_index do |v,i|
404
+ obj[i] = clean_member(v)
405
+ yield(v) if block_given?
406
+ end
407
+ else
408
+ obj.instance_variables.each do |var|
409
+ dat = obj.instance_variable_get(var)
410
+ obj.instance_variable_set(var, clean_member(dat))
411
+ yield(dat) if block_given?
412
+ end
413
+ end
414
+ # obj.instance_variables.each do |var|
415
+ # dat = obj.instance_variable_get(var)
416
+ # obj.instance_variable_set(var, clean_member(dat))
417
+ # yield(dat) if block_given?
418
+ # end
419
+ end
420
+
421
+ def clean_member(val)
422
+ if val.is_a?(Hash) || val.is_a?(Array);
423
+ if val && val.empty?
424
+ nil
425
+ elsif val && val.respond_to?(:values) && val.size == 1
426
+ val.values[0]
427
+ else
428
+ val
429
+ end
430
+ else
431
+ val
432
+ # # Probably shouldn't do this on instance-var values. ...Why not?
433
+ # if val.instance_variables.size < 1
434
+ # nil
435
+ # elsif val.instance_variables.size == 1
436
+ # val.instance_variable_get(val.instance_variables[0])
437
+ # else
438
+ # val
439
+ # end
440
+ end
441
+ end
442
+
443
+ end # Cursor
444
+
445
+
446
+
447
+ ##### SAX HANDLER #####
448
+
449
+
450
+ # A handler instance is created for each parsing run. The handler has several important functions:
451
+ # 1. Receive callbacks from the sax/stream parsing engine (start_element, end_element, attribute...).
452
+ # 2. Maintain a stack of cursors, growing & shrinking, throughout the parsing run.
453
+ # 3. Maintain a Cursor instance throughout the parsing run.
454
+ # 3. Hand over parser callbacks & data to the Cursor instance for refined processing.
455
+ #
456
+ # The handler instance is unique to each different parsing gem but inherits generic
457
+ # methods from this Handler module. During each parsing run, the Hander module creates
458
+ # a new instance of the spcified parer's handler class and runs the handler's main parsing method.
459
+ # At the end of the parsing run the handler instance, along with it's newly parsed object,
460
+ # is returned to the object that originally called for the parsing run (your script/app/whatever).
461
+ module Handler
462
+
463
+ attr_accessor :stack, :template
464
+
465
+ SaxParser.install_defaults(self)
466
+
467
+
468
+ ### Class Methods ###
469
+
470
+ # Main parsing interface (also aliased at SaxParser.parse)
471
+ def self.build(io, template=nil, initial_object=nil, parser=nil, options={})
472
+ parser = parser || options[:parser] || backend
473
+ parser = get_backend(parser)
474
+ (Rfm.log.info "Using backend parser: #{parser}, with template: #{template}") if options[:log_parser]
475
+ parser.build(io, template, initial_object)
476
+ end
477
+
478
+ def self.included(base)
479
+ # Add a .build method to the custom handler class, when the generic Handler module is included.
480
+ def base.build(io, template=nil, initial_object=nil)
481
+ handler = new(template, initial_object)
482
+ handler.run_parser(io)
483
+ handler
484
+ end
485
+ end # self.included
486
+
487
+ # Takes backend symbol and returns custom Handler class for specified backend.
488
+ def self.get_backend(parser=backend)
489
+ (parser = decide_backend) unless parser
490
+ if parser.is_a?(String) || parser.is_a?(Symbol)
491
+ parser_proc = PARSERS[parser.to_sym][:proc]
492
+ parser_proc.call unless parser_proc.nil? || const_defined?((parser.to_s.capitalize + 'Handler').to_sym)
493
+ SaxParser.const_get(parser.to_s.capitalize + "Handler")
494
+ end
495
+ rescue
496
+ raise "Could not load the backend parser '#{parser}': #{$!}"
497
+ end
498
+
499
+ # Finds a loadable backend and returns its symbol.
500
+ def self.decide_backend
501
+ #BACKENDS.find{|b| !Gem::Specification::find_all_by_name(b[1]).empty? || b[0]==:rexml}[0]
502
+ PARSERS.find{|k,v| !Gem::Specification::find_all_by_name(v[:file]).empty? || k == :rexml}[0]
503
+ rescue
504
+ raise "The xml parser could not find a loadable backend library: #{$!}"
505
+ end
506
+
507
+
508
+
509
+ ### Instance Methods ###
510
+
511
+ def initialize(_template=nil, initial_object=nil)
512
+ initial_object = case
513
+ when initial_object.nil?; default_class.new
514
+ when initial_object.is_a?(Class); initial_object.new
515
+ when initial_object.is_a?(String) || initial_object.is_a?(Symbol); SaxParser.get_constant(initial_object).new
516
+ else initial_object
517
+ end
518
+ #initial_object = initial_object || default_class.new || {}
519
+ @stack = []
520
+ @template = get_template(_template)
521
+ @tag_translation = tag_translation
522
+ #(@template = @template.values[0]) if @template.size == 1
523
+ #y @template
524
+ init_element_buffer
525
+ set_cursor Cursor.new(@template, initial_object, 'top', self)
526
+ end
527
+
528
+ # Takes string, symbol, hash, and returns a (possibly cached) parsing template.
529
+ # String can be a file name, yaml, xml.
530
+ # Symbol is a name of a template stored in SaxParser@templates (you would set the templates when your app or gem loads).
531
+ # Templates stored in the SaxParser@templates var can be strings of code, file specs, or hashes.
532
+ def get_template(name)
533
+ # dat = templates[name]
534
+ # if dat
535
+ # rslt = load_template(dat)
536
+ # else
537
+ # rslt = load_template(name)
538
+ # end
539
+ # (templates[name] = rslt) #unless dat == rslt
540
+ # The above works, but this is cleaner.
541
+ templates[name] = templates[name] && load_template(templates[name]) || load_template(name)
542
+ end
543
+
544
+ # Does the heavy-lifting of template retrieval.
545
+ def load_template(dat)
546
+ prefix = defined?(template_prefix) ? template_prefix : ''
547
+ rslt = case
548
+ when dat.is_a?(Hash); dat
549
+ when dat.to_s[/\.y.?ml$/i]; (YAML.load_file(File.join(*[prefix, dat].compact)))
550
+ # This line might cause an infinite loop.
551
+ when dat.to_s[/\.xml$/i]; self.class.build(File.join(*[prefix, dat].compact), nil, {'compact'=>true})
552
+ when dat.to_s[/^<.*>/i]; "Convert from xml to Hash - under construction"
553
+ when dat.is_a?(String); YAML.load dat
554
+ else default_class.new
555
+ end
556
+ end
557
+
558
+ def result
559
+ stack[0].object if stack[0].is_a? Cursor
560
+ end
561
+
562
+ def cursor
563
+ stack.last
564
+ end
565
+
566
+ def set_cursor(args) # cursor_object
567
+ if args.is_a? Cursor
568
+ stack.push(args)
569
+ cursor.parent = stack[-2] || stack[0] #_stack[0] so methods called on parent won't bomb.
570
+ # Cursor is no longer storing top or stack, it is delegating those mehthods to main handler.
571
+ #cursor.top = stack[0]
572
+ #cursor.stack = stack
573
+ end
574
+ cursor
575
+ end
576
+
577
+ def dump_cursor
578
+ stack.pop
579
+ end
580
+
581
+ def top
582
+ stack[0]
583
+ end
584
+
585
+ def transform(name)
586
+ return name unless @tag_translation.is_a?(Proc)
587
+ #name.to_s.gsub(*@tag_translation)
588
+ @tag_translation.call(name.to_s)
589
+ end
590
+
591
+ def init_element_buffer
592
+ @element_buffer = {:tag=>nil, :attributes=>default_class.new, :text=>''}
593
+ end
594
+
595
+ def send_element_buffer
596
+ if element_buffer?
597
+ (@element_buffer[:attributes][text_label] = @element_buffer[:text]) if @element_buffer[:text].to_s[/[^\s]/]
598
+ set_cursor cursor.receive_start_element(@element_buffer[:tag], @element_buffer[:attributes])
599
+ init_element_buffer
600
+ end
601
+ end
602
+
603
+ def element_buffer?
604
+ @element_buffer[:tag] && !@element_buffer[:tag].empty?
605
+ end
606
+
607
+
608
+ # Add a node to an existing element.
609
+ def _start_element(tag, attributes=nil, *args)
610
+ #puts ["_START_ELEMENT", tag, attributes, args].to_yaml # if tag.to_s.downcase=='fmrestulset'
611
+ tag = transform tag
612
+ send_element_buffer if element_buffer?
613
+ if attributes
614
+ # This crazy thing transforms attribute keys to underscore (or whatever).
615
+ #attributes = default_class[*attributes.collect{|k,v| [transform(k),v] }.flatten]
616
+ # This works but downcases all attribute names - not good.
617
+ attributes = default_class.new.tap {|hash| attributes.each {|k, v| hash[transform(k)] = v}}
618
+ # This doesn't work yet, but at least it wont downcase hash keys.
619
+ #attributes = Hash.new.tap {|hash| attributes.each {|k, v| hash[transform(k)] = v}}
620
+ end
621
+ @element_buffer.merge!({:tag=>tag, :attributes => attributes || default_class.new})
622
+ end
623
+
624
+ # Add attribute to existing element.
625
+ def _attribute(name, value, *args)
626
+ #puts "Receiving attribute '#{name}' with value '#{value}'"
627
+ name = transform name
628
+ new_att = default_class.new.tap{|att| att[name]=value}
629
+ @element_buffer[:attributes].merge!(new_att)
630
+ end
631
+
632
+ # Add 'content' attribute to existing element.
633
+ def _text(value, *args)
634
+ #puts "Receiving text '#{value}'"
635
+ return unless value.to_s[/[^\s]/]
636
+ @element_buffer[:text] << value
637
+ end
638
+
639
+ # Close out an existing element.
640
+ def _end_element(tag, *args)
641
+ tag = transform tag
642
+ #puts "Receiving end_element '#{tag}'"
643
+ send_element_buffer
644
+ cursor.receive_end_element(tag) and dump_cursor
645
+ end
646
+
647
+ def _doctype(*args)
648
+ (args = args[0].gsub(/"/, '').split) if args.size ==1
649
+ _start_element('doctype', :value=>args)
650
+ _end_element('doctype')
651
+ end
652
+
653
+ end # Handler
654
+
655
+
656
+
657
+ ##### SAX PARSER BACKEND HANDLERS #####
658
+
659
+ PARSERS[:libxml] = {:file=>'libxml-ruby', :proc => proc do
660
+ require 'libxml'
661
+ class LibxmlHandler
662
+ include LibXML
663
+ include XML::SaxParser::Callbacks
664
+ include Handler
665
+
666
+ def run_parser(io)
667
+ parser = case
668
+ when (io.is_a?(File) or io.is_a?(StringIO)); XML::SaxParser.io(io)
669
+ when io[/^</]; XML::SaxParser.io(StringIO.new(io))
670
+ else XML::SaxParser.io(File.new(io))
671
+ end
672
+ parser.callbacks = self
673
+ parser.parse
674
+ end
675
+
676
+ # def on_start_element_ns(name, attributes, prefix, uri, namespaces)
677
+ # attributes.merge!(:prefix=>prefix, :uri=>uri, :xmlns=>namespaces)
678
+ # _start_element(name, attributes)
679
+ # end
680
+
681
+ alias_method :on_start_element, :_start_element
682
+ alias_method :on_end_element, :_end_element
683
+ alias_method :on_characters, :_text
684
+ alias_method :on_internal_subset, :_doctype
685
+ end # LibxmlSax
686
+ end}
687
+
688
+ PARSERS[:nokogiri] = {:file=>'nokogiri', :proc => proc do
689
+ require 'nokogiri'
690
+ class NokogiriHandler < Nokogiri::XML::SAX::Document
691
+ include Handler
692
+
693
+ def run_parser(io)
694
+ parser = Nokogiri::XML::SAX::Parser.new(self)
695
+ parser.parse(case
696
+ when (io.is_a?(File) or io.is_a?(StringIO)); io
697
+ when io[/^</]; StringIO.new(io)
698
+ else File.new(io)
699
+ end)
700
+ end
701
+
702
+ alias_method :start_element, :_start_element
703
+ alias_method :end_element, :_end_element
704
+ alias_method :characters, :_text
705
+ end # NokogiriSax
706
+ end}
707
+
708
+ PARSERS[:ox] = {:file=>'ox', :proc => proc do
709
+ require 'ox'
710
+ class OxHandler < ::Ox::Sax
711
+ include Handler
712
+
713
+ def run_parser(io)
714
+ case
715
+ when (io.is_a?(File) or io.is_a?(StringIO)); Ox.sax_parse self, io
716
+ when io.to_s[/^</]; StringIO.open(io){|f| Ox.sax_parse self, f}
717
+ else File.open(io){|f| Ox.sax_parse self, f}
718
+ end
719
+ end
720
+
721
+ alias_method :start_element, :_start_element
722
+ alias_method :end_element, :_end_element
723
+ alias_method :attr, :_attribute
724
+ alias_method :text, :_text
725
+ alias_method :doctype, :_doctype
726
+ end # OxFmpSax
727
+ end}
728
+
729
+ PARSERS[:rexml] = {:file=>'rexml/document', :proc => proc do
730
+ require 'rexml/document'
731
+ require 'rexml/streamlistener'
732
+ class RexmlHandler
733
+ # Both of these are needed to use rexml streaming parser,
734
+ # but don't put them here... put them at the _top.
735
+ #require 'rexml/streamlistener'
736
+ #require 'rexml/document'
737
+ include REXML::StreamListener
738
+ include Handler
739
+
740
+ def run_parser(io)
741
+ parser = REXML::Document
742
+ case
743
+ when (io.is_a?(File) or io.is_a?(StringIO)); parser.parse_stream(io, self)
744
+ when io.to_s[/^</]; StringIO.open(io){|f| parser.parse_stream(f, self)}
745
+ else File.open(io){|f| parser.parse_stream(f, self)}
746
+ end
747
+ end
748
+
749
+ alias_method :tag_start, :_start_element
750
+ alias_method :tag_end, :_end_element
751
+ alias_method :text, :_text
752
+ alias_method :doctype, :_doctype
753
+ end # RexmlStream
754
+ end}
755
+
756
+
757
+ end # SaxParser
758
+ end # Rfm
759
+
760
+
761
+
762
+
763
+ ##### CORE ADDITIONS #####
764
+
765
+ class Object
766
+
767
+ # Master method to attach any object to this object.
768
+ def _attach_object!(obj, *args) # name/label, collision-delimiter, attachment-prefs, type, *options: <options>
769
+ #puts ["OBJECT._attach_object", self.class, obj.class, obj.to_s, args].to_yaml
770
+ default_options = {
771
+ :shared_variable_name => 'attributes',
772
+ :default_class => Hash,
773
+ :create_accessors => [] #:all, :private, :shared, :hash
774
+ }
775
+ options = default_options.merge(args.last.is_a?(Hash) ? args.pop : {}){|key, old, new| new || old}
776
+ name = (args[0] || options[:name])
777
+ delimiter = (args[1] || options[:delimiter])
778
+ prefs = (args[2] || options[:prefs])
779
+ type = (args[3] || options[:type])
780
+ #puts ['OBJECT.attach_object', type, name, self.class, obj.class, delimiter, prefs, options[:shared_variable_name], options[:default_class], options[:create_accessors]].join(', ')
781
+ case
782
+ when prefs=='none' || prefs=='cursor'; nil
783
+ when name
784
+ self._merge_object!(obj, name, delimiter, prefs, type, options)
785
+ else
786
+ self._merge_object!(obj, 'unknown_name', delimiter, prefs, type, options)
787
+ end
788
+ end
789
+
790
+ # Master method to merge any object with this object
791
+ def _merge_object!(obj, name, delimiter, prefs, type, options={})
792
+ #puts ["-----OBJECT._merge_object", self.class, (obj.to_s rescue obj.class), name, delimiter, prefs, type.capitalize, options].join(', ')
793
+ if prefs=='private'
794
+ _merge_instance!(obj, name, delimiter, prefs, type, options)
795
+ else
796
+ _merge_shared!(obj, name, delimiter, prefs, type, options)
797
+ end
798
+ end
799
+
800
+ # Merge a named object with the shared instance variable of self.
801
+ def _merge_shared!(obj, name, delimiter, prefs, type, options={})
802
+ shared_var = instance_variable_get("@#{options[:shared_variable_name]}") || instance_variable_set("@#{options[:shared_variable_name]}", options[:default_class].new)
803
+ #puts "\n-----OBJECT._merge_shared: self '#{self.class}' obj '#{obj.class}' name '#{name}' delimiter '#{delimiter}' type '#{type}' shared_var '#{options[:shared_variable_name]} - #{shared_var.class}'"
804
+ # TODO: Figure this part out:
805
+ # The resetting of shared_variable_name to 'attributes' was to fix Asset.field_controls (it was not able to find the valuelive name).
806
+ # I think there might be a level of heirarchy that is without a proper cursor model, when using shared variables & object delimiters.
807
+ shared_var._merge_object!(obj, name, delimiter, nil, type, options.merge(:shared_variable_name=>'attributes'))
808
+ end
809
+
810
+ # Merge a named object with the specified instance variable of self.
811
+ def _merge_instance!(obj, name, delimiter, prefs, type, options={})
812
+ #puts ['_merge_instance!', self.class, obj.class, name, delimiter, prefs, type, options.keys, '_end_merge_instance!'].join(', ')
813
+ if instance_variable_get("@#{name}") || delimiter
814
+ if delimiter
815
+ delimit_name = obj._get_attribute(delimiter, options[:shared_variable_name]).to_s.downcase
816
+ #puts ['_setting_with_delimiter', delimit_name]
817
+ #instance_variable_set("@#{name}", instance_variable_get("@#{name}") || options[:default_class].new)[delimit_name]=obj
818
+ # This line is more efficient than the above line.
819
+ instance_variable_set("@#{name}", options[:default_class].new) unless instance_variable_get("@#{name}")
820
+ instance_variable_get("@#{name}")[delimit_name]=obj
821
+ # Trying to handle multiple portals with same table-occurance on same layout.
822
+ # In parsing terms, this is trying to handle multiple elements who's delimiter field contains the SAME delimiter data.
823
+ #instance_variable_get("@#{name}")._merge_object!(obj, delimit_name, nil, nil, nil)
824
+ else
825
+ #puts ['_setting_existing_instance_var', name]
826
+ instance_variable_set("@#{name}", [instance_variable_get("@#{name}")].flatten << obj)
827
+ end
828
+ else
829
+ #puts ['_setting_new_instance_var', name]
830
+ instance_variable_set("@#{name}", obj)
831
+ end
832
+
833
+ # NEW
834
+ _create_accessor(name) if (options[:create_accessors] & ['all','private']).any?
835
+
836
+ end
837
+
838
+ # Get an instance variable, a member of a shared instance variable, or a hash value of self.
839
+ def _get_attribute(name, shared_var_name=nil, options={})
840
+ return unless name
841
+ #puts ["\nOBJECT_get_attribute", self.instance_variables, name, shared_var_name, options].join(', ')
842
+ (shared_var_name = options[:shared_variable_name]) unless shared_var_name
843
+
844
+ rslt = case
845
+ when self.is_a?(Hash) && self[name]; self[name]
846
+ when (var= instance_variable_get("@#{shared_var_name}")) && var[name]; var[name]
847
+ else instance_variable_get("@#{name}")
848
+ end
849
+
850
+ #puts "OBJECT#_get_attribute: name '#{name}' shared_var_name '#{shared_var_name}' options '#{options}' rslt '#{rslt}'"
851
+ rslt
852
+ end
853
+
854
+ # # We don't know which attributes are shared, so this isn't really accurate per the options.
855
+ # def _create_accessors options=[]
856
+ # options=[options].flatten.compact
857
+ # #puts ['CREATE_ACCESSORS', self.class, options, ""]
858
+ # return false if (options & ['none']).any?
859
+ # if (options & ['all', 'private']).any?
860
+ # meta = (class << self; self; end)
861
+ # meta.send(:attr_reader, *instance_variables.collect{|v| v.to_s[1,99].to_sym})
862
+ # end
863
+ # if (options & ['all', 'shared']).any?
864
+ # instance_variables.collect{|v| instance_variable_get(v)._create_accessors('hash')}
865
+ # end
866
+ # return true
867
+ # end
868
+
869
+ # NEW
870
+ def _create_accessor(name)
871
+ #puts "OBJECT._create_accessor '#{name}' for Object '#{self.class}'"
872
+ meta = (class << self; self; end)
873
+ meta.send(:attr_reader, name.to_sym)
874
+ end
875
+
876
+ # Attach hash as individual instance variables.
877
+ # This is for manually attaching a hash of attributes to the current object.
878
+ # Pass in translation procs to alter the keys or values.
879
+ def _attach_as_instance_variables(hash, options={})
880
+ #hash.each{|k,v| instance_variable_set("@#{k}", v)} if hash.is_a? Hash
881
+ key_translator = options[:key_translator]
882
+ value_translator = options[:value_translator]
883
+ #puts ["ATTACH_AS_INSTANCE_VARIABLES", key_translator, value_translator].join(', ')
884
+ if hash.is_a? Hash
885
+ hash.each do |k,v|
886
+ (k = key_translator.call(k)) if key_translator
887
+ (v = value_translator.call(v)) if value_translator
888
+ instance_variable_set("@#{k}", v)
889
+ end
890
+ end
891
+ end
892
+
893
+ end # Object
894
+
895
+ class Array
896
+ def _merge_object!(obj, name, delimiter, prefs, type, options={})
897
+ #puts ["\n+++++ARRAY._merge_object", self.class, (obj.to_s rescue obj.class), name, delimiter, prefs, type.titleize, options].join(', ')
898
+ case
899
+ when prefs=='shared' || type == 'attribute' && prefs.to_s != 'private' ; _merge_shared!(obj, name, delimiter, prefs, type, options)
900
+ when prefs=='private'; _merge_instance!(obj, name, delimiter, prefs, type, options)
901
+ else self << obj
902
+ end
903
+ end
904
+ end # Array
905
+
906
+ class Hash
907
+ def _merge_object!(obj, name, delimiter, prefs, type, options={})
908
+ #puts ["\n*****HASH._merge_object", type, name, self.class, (obj.to_s rescue obj.class), delimiter, prefs, options].join(', ')
909
+ case
910
+ when prefs=='shared'
911
+ _merge_shared!(obj, name, delimiter, prefs, type, options)
912
+ when prefs=='private'
913
+ _merge_instance!(obj, name, delimiter, prefs, type, options)
914
+ when (self[name] || delimiter)
915
+ if delimiter
916
+ delimit_name = obj._get_attribute(delimiter, options[:shared_variable_name]).to_s.downcase
917
+ #puts "MERGING delimited object with hash: self '#{self.class}' obj '#{obj.class}' name '#{name}' delim '#{delimiter}' delim_name '#{delimit_name}' options '#{options}'"
918
+ self[name] ||= options[:default_class].new
919
+ self[name][delimit_name]=obj
920
+ # This is supposed to handle multiple elements who's delimiter value is the SAME.
921
+ #obj.instance_variable_set(:@_index_, 0)
922
+ #self[name]._merge_object!(obj, delimit_name, nil, nil, nil)
923
+ else
924
+ self[name] = [self[name]].flatten
925
+ #obj.instance_variable_set(:@_index_, self[name].last.instance_variable_get(:@_index_).to_i + 1)
926
+ self[name] << obj
927
+ end
928
+ _create_accessor(name) if (options[:create_accessors] & ['all','shared','hash']).any?
929
+ else
930
+ self[name] = obj
931
+ _create_accessor(name) if (options[:create_accessors] & ['all','shared','hash']).any?
932
+ end
933
+ end
934
+
935
+ # def _create_accessors options=[]
936
+ # #puts ['CREATE_ACCESSORS_for_HASH', self.class, options]
937
+ # options=[options].flatten.compact
938
+ # super and
939
+ # if (options & ['all', 'hash']).any?
940
+ # meta = (class << self; self; end)
941
+ # keys.each do |k|
942
+ # meta.send(:define_method, k) do
943
+ # self[k]
944
+ # end
945
+ # end
946
+ # end
947
+ # end
948
+
949
+ def _create_accessor(name)
950
+ #puts "HASH._create_accessor '#{name}' for Hash '#{self.class}'"
951
+ meta = (class << self; self; end)
952
+ meta.send(:define_method, name) do
953
+ self[name]
954
+ end
955
+ end
956
+
957
+ end # Hash
958
+
959
+
960
+
961
+
962
+ # #### NOTES and TODOs ####
963
+ #
964
+ # done: Move test data & user models to spec folder and local_testing.
965
+ # done: Create special file in local_testing for experimentation & testing - will have user models, grammar-yml, calling methods.
966
+ # done: Add option to 'compact' unnecessary or empty elements/attributes - maybe - should this should be handled at Model level?
967
+ # na : Separate all attribute options in yml into 'attributes:' hash, similar to 'elements:' hash.
968
+ # done: Handle multiple 'text' callbacks for a single element.
969
+ # done: Add options for text handling (what to name, where to put).
970
+ # done: Fill in other configuration options in yml
971
+ # done: Clean_elements doesn't work if elements are non-hash/array objects. Make clean_elements work with object attributes.
972
+ # TODO: Clean_elements may not work for non-hash/array objecs with multiple instance-variables.
973
+ # na : Clean_elements may no longer work with a globally defined 'compact'.
974
+ # TODO: Do we really need Cursor#top and Cursor#stack ? Can't we get both from handler.stack. Should we store handler in cursor inst var @handler?
975
+ # TODO: When using a non-hash/array object as the initial object, things get kinda srambled.
976
+ # See Rfm::Connection, when sending self (the connection object) as the initial_object.
977
+ # done: 'compact' breaks badly with fmpxmllayout data.
978
+ # na : Do the same thing with 'ignore' as you did with 'attach', so you can enable using the 'attributes' array of the yml model.
979
+ # done: Double-check that you're pointing to the correct model/submodel, since you changed all helper-methods to look at curent-model by default.
980
+ # done: Make sure all method calls are passing the model given by the calling-method.
981
+ # abrt: Give most of the config options a global possibility. (no true global config, but top-level acts as global for all immediate submodels).
982
+ # na : Block attachment methods from seeing parent if parent isn't the current objects true parent (how?).
983
+ # done: Handle attach: hash better (it's not specifically handled, but declaring it will block a parents influence).
984
+ # done: CaseInsensitiveHash/IndifferentAccess is not working for sax parser.
985
+ # na : Give the yml (and xml) doc the ability to have a top-level hash like "fmresultset" or "fmresultset_yml" or "fmresultset_xml",
986
+ # then you have a label to refer to it if you load several config docs at once (like into a Rfm::SaxParser::TEMPLATES constant).
987
+ # Use an array of accepted model-keys to filter whether loaded template is a named-model or actual model data.
988
+ # done: Load up all template docs when Rfm loads, or when Rfm::SaxParser loads. For RFM only, not for parser module.
989
+ # done: Move SaxParser::Handler class methods to SaxParser, so you can do Rfm::SaxParser.parse(io, backend, template, initial_object)
990
+ # done: Switch args order in .build methods to (io, template, initial_object, backend)
991
+ # done: Change "grammar" to "template" in all code
992
+ # done: Change 'cursor._' methods to something more readable, since they will be used in Rfm and possibly user models.
993
+ # done: Split off template loading into load_templates and/or get_templates methods.
994
+ # TODO: Something is downcasing somewhere - see the fmpxmllayout response. Looks like 'compact' might have something to do with it.
995
+ # done: Make attribute attachment default to individual.
996
+ # done: 'attach: shared' doesnt work yet for elements.
997
+ # na : Arrays should always get elements attached to their records and attributes attached to their instance variables.
998
+ # done: Merge 'ignore: self, elements, attributes' config into 'attach: ignore, attach_elements: ignore, attach_attributes: ignore'.
999
+ # done: Consider having one single group of methods to attach all objects (elements OR attributes OR text) to any given parent object.
1000
+ # na : May need to store 'ignored' models in new cursor, with the parent object instead of the new object. Probably not a good idea
1001
+ # done: Fix label_or_tag for object-attachment.
1002
+ # done: Fix delineate_with_hash in parsing of resultset field_meta (should be hash of hashes, not array of hashes).
1003
+ # done: Test new parser with raw data from multiple sources, make sure it works as raw.
1004
+ # done: Make sure single-attribute (or text) handler has correct objects & models to work with.
1005
+ # na : Rewrite attach_to_what? logic to start with base_object type, then have sub-case statements for the rest.
1006
+ # done: Build backend-gem loading scheme. Eliminate gem 'require'. Use catch/throw like in XmlParser.
1007
+ # done: Splash_sax.rb is throwing error when loading User.all when SaxParser.backend is anything other than :ox.
1008
+ # This is probably related to the issue of incomming attributes (that are after the incoming start_element) not knowing their model.
1009
+ # Some multiple-text attributes were tripping up delineate_with_hash, so I added some fault-tollerance to that method.
1010
+ # But those multi-text attributes are still way ugly when passed by libxml or nokogiri. Ox & Rexml are fine and pass them as one chunk.
1011
+ # Consider buffering all attributes & text until start of new element.
1012
+ # YAY : I bufffered all attributes & text, and the problem is solved.
1013
+ # na : Can't configure an attribute (in template) if it is used in delineate_with_hash. (actually it works if you specifiy the as_name: correctly).
1014
+ # TODO: Some configurations in template cause errors - usually having to do with nil. See below about eliminating all 'rescue's .
1015
+ # done: Can't supply custom class if it's a String (but an unspecified subclass of plain Object works fine!?).
1016
+ # TODO: Attaching non-hash/array object to Array will thro error. Is this actually fixed?
1017
+ # done?: Sending elements with subelements to Shared results in no data attached to the shared var.
1018
+ # TODO: compact is not working for fmpxmllayout-error. Consider rewrite of 'compact' method, or allow compact to work on end_element with no matching tag.
1019
+ # mabe: Add ability to put a regex in the as_name parameter, that will operate on the tag/label/name.
1020
+ # TODO: Optimize:
1021
+ # Use variables, not methods.
1022
+ # Use string interpolation not concatenation.
1023
+ # Use destructive! operations (carefully). Really?
1024
+ # Get this book: http://my.safaribooksonline.com/9780321540034?portal=oreilly
1025
+ # See this page: http://www.igvita.com/2008/07/08/6-optimization-tips-for-ruby-mri/
1026
+ # done: Consider making default attribute-attachment shared, instead of instance.
1027
+ # done: Find a way to get SaxParser defaults into core class patches.
1028
+ # done: Check resultset portal_meta in Splash Asset model for field-definitions coming out correct according to sax template.
1029
+ # done: Make sure Rfm::Metadata::Field is being used in portal-definitions.
1030
+ # done: Scan thru Rfm classes and use @instance variables instead of their accessors, so sax-parser does less work.
1031
+ # done: Change 'instance' option to 'private'. Change 'shared' to <whatever>.
1032
+ # done: Since unattached elements won't callback, add their callback to an array of procs on the current cursor at the beginning
1033
+ # of the non-attached tag.
1034
+ # TODO: Handle check-for-errors in non-resultset loads.
1035
+ # abrt: Figure out way to get doctype from nokogiri. Tried, may be practically impossible.
1036
+ # TODO: Clean up sax code so that no 'rescue' is needed - if an error happens it should be a legit error outside of SaxParser.
1037
+ # TODO: Allow attach:none when using handler.
1038
+
1039
+