Module: HappyMapper::ClassMethods

Defined in:
lib/happymapper/class_methods.rb

Overview

Class methods to be applied to classes that include the HappyMapper module.

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.



246
247
248
# File 'lib/happymapper/class_methods.rb', line 246

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.



167
168
169
# File 'lib/happymapper/class_methods.rb', line 167

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

#after_parse_callbacksObject

The list of registered after_parse callbacks.



158
159
160
# File 'lib/happymapper/class_methods.rb', line 158

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



23
24
25
26
27
# File 'lib/happymapper/class_methods.rb', line 23

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.



35
36
37
# File 'lib/happymapper/class_methods.rb', line 35

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



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

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

#defined_contentObject



365
366
367
# File 'lib/happymapper/class_methods.rb', line 365

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



78
79
80
81
82
# File 'lib/happymapper/class_methods.rb', line 78

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.



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

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:



151
152
153
# File 'lib/happymapper/class_methods.rb', line 151

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:



137
138
139
# File 'lib/happymapper/class_methods.rb', line 137

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.



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

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.



179
180
181
182
# File 'lib/happymapper/class_methods.rb', line 179

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.



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
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
# File 'lib/happymapper/class_methods.rb', line 268

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



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

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

Raises:



187
188
189
190
191
192
193
# File 'lib/happymapper/class_methods.rb', line 187

def tag(new_tag_name)
  return if new_tag_name.nil? || (name = new_tag_name.to_s).empty?

  raise SyntaxError, "Unexpected ':' in tag name #{new_tag_name}" if name.include? ":"

  @tag_name = name
end

#tag_nameString

The name of the tag

Returns:

  • (String)

    the name of the tag as a string, downcased



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

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



255
256
257
# File 'lib/happymapper/class_methods.rb', line 255

def with_nokogiri_config(&blk)
  @nokogiri_config_callback = blk
end

#wrap(name, options = {}, &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



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/happymapper/class_methods.rb', line 211

def wrap(name, options = {}, &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|
    method_name = item.method_name
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{method_name}                                    # def property
        @#{name} ||=                                        #   @wrapper ||=
          wrapper_anonymous_classes['#{wrapper_key}'].new   #     wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
        @#{name}.#{method_name}                             #   @wrapper.property
      end                                                   # end

      def #{method_name}=(value)                            # def property=(value)
        @#{name} ||=                                        #   @wrapper ||=
          wrapper_anonymous_classes['#{wrapper_key}'].new   #     wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
        @#{name}.#{method_name} = value                     #   @wrapper.property = value
      end                                                   # end
    RUBY
  end

  has_one name, wrapper, options
end