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.
- checksums.yaml +15 -0
- data/CHANGELOG.md +45 -16
- data/README.md +251 -274
- data/lib/rfm.rb +42 -20
- data/lib/rfm/VERSION +1 -1
- data/lib/rfm/base.rb +63 -196
- data/lib/rfm/database.rb +15 -16
- data/lib/rfm/layout.rb +244 -271
- data/lib/rfm/metadata/datum.rb +45 -0
- data/lib/rfm/metadata/field.rb +33 -13
- data/lib/rfm/metadata/field_control.rb +57 -25
- data/lib/rfm/metadata/layout_meta.rb +38 -0
- data/lib/rfm/metadata/resultset_meta.rb +66 -0
- data/lib/rfm/metadata/value_list_item.rb +7 -6
- data/lib/rfm/record.rb +54 -74
- data/lib/rfm/resultset.rb +63 -112
- data/lib/rfm/server.rb +6 -172
- data/lib/rfm/utilities/config.rb +100 -55
- data/lib/rfm/utilities/connection.rb +209 -0
- data/lib/rfm/utilities/core_ext.rb +14 -1
- data/lib/rfm/utilities/factory.rb +68 -65
- data/lib/rfm/utilities/sax_parser.rb +1039 -0
- metadata +154 -206
- data/lib/rfm/utilities/fmpxmlresult.rb +0 -167
- data/lib/rfm/utilities/fmresultset.rb +0 -153
- data/lib/rfm/utilities/xml_parser.rb +0 -124
- data/lib/rfm/xml_mini/hpricot.rb +0 -133
- data/lib/rfm/xml_mini/ox_sax.rb +0 -91
- data/lib/rfm/xml_mini/rexml_sax.rb +0 -81
@@ -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
|
+
|