Module: HappyMapper::ClassMethods

Defined in:
lib/happymapper.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#nokogiri_config_callbackProc (readonly)

The callback defined through #with_nokogiri_config.

Returns:

  • (Proc)

    the proc to pass to Nokogiri to setup parse options. nil if empty.


269
270
271
# File 'lib/happymapper.rb', line 269

def nokogiri_config_callback
  @nokogiri_config_callback
end

Instance Method Details

#after_parse {|object| ... } ⇒ Object

Register a new after_parse callback, given as a block.

Yields:

  • (object)

    Yields the newly-parsed object to the block after parsing. Sub-objects will be already populated.


199
200
201
# File 'lib/happymapper.rb', line 199

def after_parse(&block)
  after_parse_callbacks.push(block)
end

#after_parse_callbacksObject

The list of registered after_parse callbacks.


190
191
192
# File 'lib/happymapper.rb', line 190

def after_parse_callbacks
  @after_parse_callbacks ||= []
end

#attribute(name, type, options = {}) ⇒ Object

The xml has the following attributes defined.

Examples:


"<country code='de'>Germany</country>"

# definition of the 'code' attribute within the class
attribute :code, String

Parameters:

  • name (Symbol)

    the name of the accessor that is created

  • type (String, Class)

    the class name of the name of the class whcih the object will be converted upon parsing

  • options (Hash) (defaults to: {})

    additional parameters to send to the relationship


55
56
57
58
59
# File 'lib/happymapper.rb', line 55

def attribute(name, type, options = {})
  attribute = Attribute.new(name, type, options)
  @attributes[name] = attribute
  attr_accessor attribute.method_name.intern
end

#attributesArray<Attribute>

The elements defined through #attribute.

Returns:

  • (Array<Attribute>)

    a list of the attributes defined for this class; an empty array is returned when there have been no attributes defined.


67
68
69
# File 'lib/happymapper.rb', line 67

def attributes
  @attributes.values
end

#content(name, type = String, options = {}) ⇒ Object

The value stored in the text node of the current element.

Examples:


"<firstName>Michael Jackson</firstName>"

# definition of the 'firstName' text node within the class

content :first_name, String

Parameters:

  • name (Symbol)

    the name of the accessor that is created

  • type (String, Class) (defaults to: String)

    the class name of the name of the class whcih the object will be converted upon parsing. By Default String class will be taken.

  • options (Hash) (defaults to: {})

    additional parameters to send to the relationship


143
144
145
146
# File 'lib/happymapper.rb', line 143

def content(name, type = String, options = {})
  @content = TextNode.new(name, type, options)
  attr_accessor @content.method_name.intern
end

#element(name, type, options = {}) ⇒ Object

An element defined in the XML that is parsed.

Examples:


"<address location='home'>
   <city>Oldenburg</city>
 </address>"

# definition of the 'city' element within the class

element :city, String

Parameters:

  • name (Symbol)

    the name of the accessor that is created

  • type (String, Class)

    the class name of the name of the class whcih the object will be converted upon parsing

  • options (Hash) (defaults to: {})

    additional parameters to send to the relationship


110
111
112
113
114
# File 'lib/happymapper.rb', line 110

def element(name, type, options = {})
  element = Element.new(name, type, options)
  attr_accessor element.method_name.intern unless @elements[name]
  @elements[name] = element
end

#elementsArray<Element>

The elements defined through #element, #has_one, and #has_many.

Returns:

  • (Array<Element>)

    a list of the elements contained defined for this class; an empty array is returned when there have been no elements defined.


123
124
125
# File 'lib/happymapper.rb', line 123

def elements
  @elements.values
end

#has_many(name, type, options = {}) ⇒ Object

The object has many of these elements in the XML.

Parameters:

  • name (Symbol)

    the name of accessor that is created

  • type (String, Class)

    the class name or the name of the class which the object will be converted upon parsing.

  • options (Hash) (defaults to: {})

    additional parameters to send to the relationship

See Also:


183
184
185
# File 'lib/happymapper.rb', line 183

def has_many(name, type, options = {})
  element name, type, { single: false }.merge(options)
end

#has_one(name, type, options = {}) ⇒ Object

The object has one of these elements in the XML. If there are multiple, the last one will be set to this value.

Parameters:

  • name (Symbol)

    the name of the accessor that is created

  • type (String, Class)

    the class name of the name of the class whcih the object will be converted upon parsing

  • options (Hash) (defaults to: {})

    additional parameters to send to the relationship

See Also:


169
170
171
# File 'lib/happymapper.rb', line 169

def has_one(name, type, options = {})
  element name, type, { single: true }.merge(options)
end

#has_xml_contentObject

Sets the object to have xml content, this will assign the XML contents that are parsed to the attribute accessor xml_content. The object will respond to the method #xml_content and will return the XML data that it has parsed.


154
155
156
# File 'lib/happymapper.rb', line 154

def has_xml_content
  attr_accessor :xml_content
end

#namespace(namespace = nil) ⇒ Object

Specify a namespace if a node and all its children are all namespaced elements. This is simpler than passing the :namespace option to each defined element.

Parameters:

  • namespace (String) (defaults to: nil)

    the namespace to set as default for the class element.


211
212
213
214
# File 'lib/happymapper.rb', line 211

def namespace(namespace = nil)
  @namespace = namespace if namespace
  @namespace if defined? @namespace
end

#parse(xml, options = {}) ⇒ Object

Parameters:

  • xml (Nokogiri::XML::Node, Nokogiri:XML::Document, String)

    the XML contents to convert into Object.

  • options (Hash) (defaults to: {})

    additional information for parsing. :single => true if requesting a single object, otherwise it defaults to retuning an array of multiple items. :xpath information where to start the parsing :namespace is the namespace to use for additional information.


290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/happymapper.rb', line 290

def parse(xml, options = {})
  # create a local copy of the objects namespace value for this parse execution
  namespace = (@namespace if defined? @namespace)

  # If the XML specified is an Node then we have what we need.
  if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
    node = xml
  else

    unless xml.is_a?(Nokogiri::XML::Document)
      # Attempt to parse the xml value with Nokogiri XML as a document
      # and select the root element
      xml = Nokogiri::XML(
        xml, nil, nil,
        Nokogiri::XML::ParseOptions::STRICT,
        &nokogiri_config_callback
      )
    end
    # Now xml is certainly an XML document: Select the root node of the document
    node = xml.root

    # if the node name is equal to the tag name then the we are parsing the
    # root element and that is important to record so that we can apply
    # the correct xpath on the elements of this document.

    root = node.name == tag_name
  end

  # if any namespaces have been provied then we should capture those and then
  # merge them with any namespaces found on the xml node and merge all that
  # with any namespaces that have been registered on the object

  namespaces = options[:namespaces] || {}
  namespaces = namespaces.merge(xml.collect_namespaces) if xml.respond_to?(:collect_namespaces)
  namespaces = namespaces.merge(@registered_namespaces)

  # if a namespace has been provided then set the current namespace to it
  # or set the default namespace to the one defined under 'xmlns'
  # or set the default namespace to the namespace that matches 'happymapper's

  if options[:namespace]
    namespace = options[:namespace]
  elsif namespaces.has_key?('xmlns')
    namespace ||= 'xmlns'
  end

  # from the options grab any nodes present and if none are present then
  # perform the following to find the nodes for the given class

  nodes = options.fetch(:nodes) do
    # when at the root use the xpath '/' otherwise use a more gready './/'
    # unless an xpath has been specified, which should overwrite default
    # and finally attach the current namespace if one has been defined
    #

    xpath  = (root ? '/' : './/')
    xpath  = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
    xpath += "#{namespace}:" if namespace

    nodes = []

    # when finding nodes, do it in this order:
    # 1. specified tag if one has been provided
    # 2. name of element
    # 3. tag_name (derived from class name by default)

    # If a tag has been provided we need to search for it.

    if options.key?(:tag)
      begin
        nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
      rescue StandardError
        nil
        # This exception takes place when the namespace is often not found
        # and we should continue on with the empty array of nodes.
      end
    else

      # This is the default case when no tag value is provided.
      # First we use the name of the element `items` in `has_many items`
      # Second we use the tag name which is the name of the class cleaned up

      [options[:name], tag_name].compact.each do |xpath_ext|
        begin
          nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
        rescue StandardError
          break
          # This exception takes place when the namespace is often not found
          # and we should continue with the empty array of nodes or keep looking
        end
        break if nodes && !nodes.empty?
      end

    end

    nodes
  end

  # Nothing matching found, we can go ahead and return
  return (options[:single] || root ? nil : []) if nodes.size == 0

  # If the :limit option has been specified then we are going to slice
  # our node results by that amount to allow us the ability to deal with
  # a large result set of data.

  limit = options[:in_groups_of] || nodes.size

  # If the limit of 0 has been specified then the user obviously wants
  # none of the nodes that we are serving within this batch of nodes.

  return [] if limit == 0

  collection = []

  nodes.each_slice(limit) do |slice|
    part = slice.map do |n|
      # If an existing HappyMapper object is provided, update it with the
      # values from the xml being parsed.  Otherwise, create a new object

      obj = options[:update] ? options[:update] : new

      attributes.each do |attr|
        value = attr.from_xml_node(n, namespace, namespaces)
        value = attr.default if value.nil?
        obj.send("#{attr.method_name}=", value)
      end

      elements.each do |elem|
        obj.send("#{elem.method_name}=", elem.from_xml_node(n, namespace, namespaces))
      end

      if (defined? @content) && @content
        obj.send("#{@content.method_name}=", @content.from_xml_node(n, namespace, namespaces))
      end

      # If the HappyMapper class has the method #xml_value=,
      # attr_writer :xml_value, or attr_accessor :xml_value then we want to
      # assign the current xml that we just parsed to the xml_value

      if obj.respond_to?('xml_value=')
        obj.xml_value = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
      end

      # If the HappyMapper class has the method #xml_content=,
      # attr_write :xml_content, or attr_accessor :xml_content then we want to
      # assign the child xml that we just parsed to the xml_content

      if obj.respond_to?('xml_content=')
        n = n.children if n.respond_to?(:children)
        obj.xml_content = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
      end

      # Call any registered after_parse callbacks for the object's class

      obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }

      # collect the object that we have created

      obj
    end

    # If a block has been provided and the user has requested that the objects
    # be handled in groups then we should yield the slice of the objects to them
    # otherwise continue to lump them together

    if block_given? && options[:in_groups_of]
      yield part
    else
      collection += part
    end
  end

  # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
  nodes = nil

  # If the :single option has been specified or we are at the root element
  # then we are going to return the first item in the collection. Otherwise
  # the return response is going to be an entire array of items.

  if options[:single] || root
    collection.first
  else
    collection
  end
end

#register_namespace(name, href) ⇒ Object

Register a namespace that is used to persist the object namespace back to XML.

Examples:


register_namespace 'prefix', 'http://www.unicornland.com/prefix'

# the output will contain the namespace defined

"<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
...
</outputXML>"

Parameters:

  • name (String)

    the xml prefix

  • href (String)

    url for the xml namespace


88
89
90
# File 'lib/happymapper.rb', line 88

def register_namespace(name, href)
  @registered_namespaces.merge!(name => href)
end

#tag(new_tag_name) ⇒ Object

Parameters:

  • new_tag_name (String)

    the name for the tag


219
220
221
# File 'lib/happymapper.rb', line 219

def tag(new_tag_name)
  @tag_name = new_tag_name.to_s unless new_tag_name.nil? || new_tag_name.to_s.empty?
end

#tag_nameString

The name of the tag

Returns:

  • (String)

    the name of the tag as a string, downcased


228
229
230
# File 'lib/happymapper.rb', line 228

def tag_name
  @tag_name ||= to_s.split('::')[-1].downcase
end

#with_nokogiri_config(&blk) ⇒ Object

Register a config callback according to the block Nokogori expects when calling Nokogiri::XML::Document.parse().

See nokogiri.org/Nokogiri/XML/Document.html#method-c-parse

Parameters:

  • the (Proc)

    proc to pass to Nokogiri to setup parse options


278
279
280
# File 'lib/happymapper.rb', line 278

def with_nokogiri_config(&blk)
  @nokogiri_config_callback = blk
end

#wrap(name, &blk) ⇒ Object

There is an XML tag that needs to be known for parsing and should be generated during a to_xml. But it doesn't need to be a class and the contained elements should be made available on the parent class

Parameters:

  • name (String)

    the name of the element that is just a place holder

  • blk (Proc)

    the element definitions inside the place holder tag


239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/happymapper.rb', line 239

def wrap(name, &blk)
  # Get an anonymous HappyMapper that has 'name' as its tag and defined
  # in '&blk'.  Then save that to a class instance variable for later use
  wrapper = AnonymousWrapperClassFactory.get(name, &blk)
  wrapper_key = wrapper.inspect
  @wrapper_anonymous_classes[wrapper_key] = wrapper

  # Create getter/setter for each element and attribute defined on the anonymous HappyMapper
  # onto this class. They get/set the value by passing thru to the anonymous class.
  passthrus = wrapper.attributes + wrapper.elements
  passthrus.each do |item|
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{item.method_name}
        @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper_key}'].new
        @#{name}.#{item.method_name}
      end
      def #{item.method_name}=(value)
        @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper_key}'].new
        @#{name}.#{item.method_name} = value
      end
    RUBY
  end

  has_one name, wrapper
end