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.



271
272
273
# File 'lib/happymapper.rb', line 271

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.



201
202
203
# File 'lib/happymapper.rb', line 201

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

#after_parse_callbacksObject

The list of registered after_parse callbacks.



192
193
194
# File 'lib/happymapper.rb', line 192

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



57
58
59
60
61
# File 'lib/happymapper.rb', line 57

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.



69
70
71
# File 'lib/happymapper.rb', line 69

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



145
146
147
148
# File 'lib/happymapper.rb', line 145

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

#defined_contentObject



387
388
389
# File 'lib/happymapper.rb', line 387

def defined_content
  @content if defined? @content
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



112
113
114
115
116
# File 'lib/happymapper.rb', line 112

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.



125
126
127
# File 'lib/happymapper.rb', line 125

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:



185
186
187
# File 'lib/happymapper.rb', line 185

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:



171
172
173
# File 'lib/happymapper.rb', line 171

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.



156
157
158
# File 'lib/happymapper.rb', line 156

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.



213
214
215
216
# File 'lib/happymapper.rb', line 213

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.



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
# File 'lib/happymapper.rb', line 293

def parse(xml, options = {})
  # Capture any provided namespaces and merge in any namespaces that have
  # been registered on the object.
  namespaces = options[:namespaces] || {}
  namespaces = namespaces.merge(@registered_namespaces)

  # 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

    # merge any namespaces found on the xml node into the namespace hash
    namespaces = namespaces.merge(xml.collect_namespaces)

    # 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 the :single option has been specified or we are at the root element
  # then we are going to return a single element or nil if no nodes are found
  single = root || options[:single]

  # if a namespace has been provided then set the current namespace to it
  # or use the namespace provided by the class
  # or use the 'xmlns' namespace if defined

  namespace = options[:namespace] || self.namespace || namespaces.key?('xmlns') && 'xmlns'

  # 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
    find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
  end

  # Nothing matching found, we can go ahead and return
  return (single ? nil : []) if nodes.empty?

  # 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|
      parse_node(n, options, namespace, namespaces)
    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

  # If we're parsing a single 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 single
    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



90
91
92
# File 'lib/happymapper.rb', line 90

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



221
222
223
# File 'lib/happymapper.rb', line 221

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



230
231
232
# File 'lib/happymapper.rb', line 230

def tag_name
  @tag_name ||= name && 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



280
281
282
# File 'lib/happymapper.rb', line 280

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



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

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