Module: OxMlk::ClassMethods

Defined in:
lib/oxmlk.rb

Overview

This class defines the annotation methods that are mixed into your Ruby classes for XML mapping information and behavior. See ox_tag, ox_attr and ox_elem for available annotations.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#ox_attrsObject

Returns the value of attribute ox_attrs.



37
38
39
# File 'lib/oxmlk.rb', line 37

def ox_attrs
  @ox_attrs
end

#ox_elemsObject

Returns the value of attribute ox_elems.



37
38
39
# File 'lib/oxmlk.rb', line 37

def ox_elems
  @ox_elems
end

#tag_procObject

Returns the value of attribute tag_proc.



37
38
39
# File 'lib/oxmlk.rb', line 37

def tag_proc
  @tag_proc
end

Instance Method Details

#from_xml(data) ⇒ Object

Returns a new instance from XML

Parameters:

Returns:

  • New instance generated from xml data passed in. Attr and Elem definitions are used to translate the xml to new object.



405
406
407
408
409
410
411
412
413
# File 'lib/oxmlk.rb', line 405

def from_xml(data)
  xml = XML::Node.from(data)
  raise 'invalid XML' unless xml.name == ox_tag
  
  returning new do |ox|
    (ox_attrs + ox_elems).each {|e| ox.send(e.setter,e.from_xml(xml))}
    ox.send(:after_parse) if ox.respond_to?(:after_parse)
  end
end

#ox?Boolean

Used to tell if this is an OxMlk Object.

Returns:

  • (Boolean)


396
397
398
# File 'lib/oxmlk.rb', line 396

def ox?
  true
end

#ox_accessor(sym) ⇒ Object



415
416
417
418
419
420
421
422
423
424
# File 'lib/oxmlk.rb', line 415

def ox_accessor(sym)
  define_method(sym) do
    @attributes ||= {}
    @attributes[sym.to_s]
  end
  define_method("#{sym}=") do |val|
    @attributes ||= {}
    @attributes[sym.to_s] = val
  end
end

#ox_attr(name, o = {}, &block) ⇒ Object

Declares a reference to a certain xml attribute.

Sym Option

sym

Symbol representing the name of the accessor

Default naming

This is what it will use to lookup the attribute if a name isn’t defined in :from. For example:

ox_attr :bob
ox_attr :pony

are equivalent to:

ox_attr :bob, :from => 'bob'
ox_attr :pony, :from => 'pony'

Boolean attributes

If the name ends in a ?, OxMlk will attempt to coerce the value to true or false, with true, yes, t and 1 mapping to true and false, no, f and 0 mapping to false, as shown below:

ox_elem :desirable?
ox_attr :bizzare?, :from => 'BIZZARE'

x = #from_xml(%{
  <object BIZZARE="1">
    <desirable>False</desirable>
  </object>
})

x.desirable?
=> false
x.bizzare?
=> true

When a block is provided the value will be passed to the block where unexpected values can be handled. If no block is provided the unexpected value will be returned.

#from_xml(%{
  <object BIZZARE="Dunno"\>
}).desirable?
=> "Dunno"

ox_attr :bizzare? do |val|
  val.upcase
end

#from_xml(%{
  <object BIZZARE="Dunno"\>
}).strange?
=> "DUNNO"

Blocks

You may also pass a block which manipulates the associated parsed value.

class Muffins
  include OxMlk

  ox_attr(:count, :from => 'bakers_dozens') {|val| val.to_i * 13 }
end

Blocks are always passed the value after being manipulated by the :as option and are the last thing to manipulate the XML. This is different than the ox_elem annotation that is passed an Array.

Options

:as

Basic Types

Allows you to specify one of several basic types to return the value as. For example

ox_elem :count, :as => Integer

is equivalent to:

ox_elem(:count) {|val| Integer(val) unless val.empty?}

Such block shorthands for Integer, Float, String, Symbol and Time are currently available.

If an Array is passed each Type in the array will be applied to the value in order.

:from

The name by which the xml value will be found in XML. Default is sym.



123
124
125
126
127
# File 'lib/oxmlk.rb', line 123

def ox_attr(name,o={},&block)
  new_attr =  Attr.new(name, o.reverse_merge(:tag_proc => @tag_proc),&block)
  @ox_attrs << new_attr
  ox_accessor new_attr.accessor
end

#ox_elem(name, o = {}, &block) ⇒ Object

Declares a reference to a certain xml element or a typed collection of elements.

Sym Option

sym

Symbol representing the name of the accessor.

Default naming

This name will be the default node searched for, if no other is declared. For example:

ox_elem :bob
ox_elem :pony

are equivalent to:

ox_elem :bob, :from => 'bob'
ox_elem :pony, :from => 'pony'

Boolean attributes

If the name ends in a ?, OxMlk will attempt to coerce the value to true or false, with true, yes, t and 1 mapping to true and false, no, f and 0 mapping to false, as shown below:

ox_elem :desirable?
ox_attr :bizzare?, :from => 'BIZZARE'

x = #from_xml(%{
  <object BIZZARE="1">
    <desirable>False</desirable>
  </object>
})

x.desirable?
=> false
x.bizzare?
=> true

When a block is provided the value will be passed to the block where unexpected values can be handled. If no block is provided the unexpected value will be returned.

#from_xml(%{
  <object>
    <desirable>Dunno</desirable>
  </object>
}).desirable?
=> "Dunno"

ox_elem :strange? do |val|
  val.upcase
end

#from_xml(%{
  <object>
    <strange>Dunno</strange>
  </object>
}).strange?
=> "DUNNO"

Blocks

You may also pass a block which manipulates the associated parsed value.

class Muffins
  include OxMlk

  ox_elem(:count, :from => 'bakers_dozens') {|val| val.first.to_i * 13 }
end

Blocks are always passed an Array and are the last thing to manipulate the XML. The array will include all elements already manipulated by anything passed to the :as option. If the :as option is an Array and no block is passed then the Array is returned unmodified. If the :as option is nil or is not an Array then only the first element is

Options

:as

Basic Types

Allows you to specify one of several basic types to return the value as. For example

ox_elem :count, :as => Integer

is equivalent to:

ox_elem(:count) {|val| Integer(val.first) unless val.first.empty?}

Such block shorthands for Integer, Float, String, Symbol and Time are currently available.

To reference many elements, put the desired type in a literal array. e.g.:

ox_elem :counts, :as => [Integer]

Even an array of text nodes can be specified with :as => []

ox_elem :quotes, :as => []

Other OxMlk Classes

Declares an accessor that represents another OxMlk class as child XML element (one-to-one or composition) or array of child elements (one-to-many or aggregation) of this type. Default is one-to-one. For one-to-many, simply pass the class as the only element in an array. You can also put several OxMlk classes in an Array that will act like a polymorphic one-to-many association.

Composition example:

<book>
 <publisher>
   <name>Pragmatic Bookshelf</name>
 </publisher>
</book>

Can be mapped using the following code:

class Book
  include OxMlk
  ox_elem :publisher, :as => Publisher
end

Aggregation example:

<library>
 <books>
  <book/>
  <book/>
 </books>
</library>

Can be mapped using the following code:

class Library
  include OxMlk
  ox_elem :books, :as => [Book], :in => 'books'
end

If you don’t have the <books> tag to wrap around the list of <book> tags:

<library>
  <name>Ruby books</name>
  <book/>
  <book/>
</library>

You can skip the wrapper argument:

ox_elem :books, :as => [Book]

Hash

Unlike ROXML, OxMlk doesn’t do anything special for hashes. However OxMlk applies :as to each element and the block once to the Array of elements. This means OxMlk can support setting hashes but it is more of a manual process. I am looking for a easier method but for now here are a few examples:

Hash of element contents

For xml such as this:

<dictionary>
  <definition>
    <word/>
    <meaning/>
  </definition>
  <definition>
    <word/>
    <meaning/>
  </definition>
</dictionary>

This is actually one of the more complex ones. It uses the :raw keyword to pass the block an array of LibXML::XML::Nodes

ox_elem(:definitions, :from => 'definition', :as => [:raw]) do |vals|
  Hash[*vals.map {}|val| [val.search('word').content,val.search('meaning').content]}.flatten]
end
Hash of :content

For xml such as this:

<dictionary>
  <definition word="quaquaversally">adjective: (of a geological formation) sloping downward from the center in all directions.</definition>
  <definition word="tergiversate">To use evasions or ambiguities; equivocate.</definition>
</dictionary>

This one can also be accomplished with the :raw keyword and a fancy block.

ox_elem(:definitions, :from => 'definition', :as => [:raw]) {|x| Hash[*x.map {|val| [val['word'],val.content] }.flatten]}
Hash of :name

For xml such as this:

<dictionary>
  <quaquaversally>adjective: (of a geological formation) sloping downward from the center in all directions.</quaquaversally>
  <tergiversate>To use evasions or ambiguities; equivocate.</tergiversate>
</dictionary>

This one requires some fancy xpath in :from to grab all the elements

ox_elem(:definitions, :from => './*', :as => [:raw]) {|x| Hash[*x.map {|val| [val.name,val.content] }.flatten]}

:from

The name by which the xml value will be found in XML. Default is sym.

This value may also include XPath notation.

:from => :content

When :from is set to :content, this refers to the content of the current node, rather than a sub-node. It is equivalent to :from => ‘.’

Example:

class Contributor
  ox_elem :name, :from => :content
  ox_attr :role
end

To map:

<contributor role="editor">James Wick</contributor>

:in

An optional name of a wrapping tag for this XML accessor. This can include other xpath values, which will be joined with :from with a ‘/’



335
336
337
338
339
# File 'lib/oxmlk.rb', line 335

def ox_elem(name,o={},&block)
  new_elem =  Elem.new(name, o.reverse_merge(:tag_proc => @tag_proc),&block)
  @ox_elems << new_elem
  ox_accessor new_elem.accessor
end

#ox_tag(tag = nil, &block) ⇒ Object

Sets the name of the XML element that represents this class. Use this to override the default camelcase class name.

Example:

class BookWithPublisher
 ox_tag 'book'
end

Without the ox_tag annotation, the XML mapped tag would have been ‘BookWithPublisher’.

Most xml documents have a consistent naming convention, for example, the node and and attribute names might appear in CamelCase. ox_tag enables you to adapt the oxmlk default names for this object to suit this convention. For example, if I had a document like so:

<XmlDoc>
  <MyPreciousData />
  <MoreToSee />
</XmlDoc>

Then I could access it’s contents by defining the following class:

class XmlDoc
  include OxMlk

  ox_tag :camelcase
  ox_elem :my_precious_data
  ox_elem :more_to_see
end

You may supply a block or any #to_proc-able object as the argument, and it will be called against the default node and attribute names before searching the document. Here are some example declaration:

ox_tag :upcase
ox_tag &:camelcase
ox_tag {|val| val.gsub('_', '').downcase }

See ActiveSupport::CoreExtensions::String::Inflections for more prepackaged formats



380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/oxmlk.rb', line 380

def ox_tag(tag=nil,&block)
  raise 'you can only set tag or a block, not both.' if tag && block

  @base_tag ||= self.to_s.split('::').last
  @ox_tag ||= case tag
  when String
    tag
  when Proc, Symbol, nil
    @tag_proc = (block || tag || :to_s).to_proc
    @tag_proc.call(@base_tag) rescue tag.to_s
  else
    raise 'you passed something weird'
  end
end

#to_xml(data) ⇒ XML::Node

Returns XML generated from an instance based on Attr & Elem definitions.

Parameters:

  • data (Object)

    An instance used to populate XML

Returns:



429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/oxmlk.rb', line 429

def to_xml(data)
  ox = XML::Node.new(ox_tag)
  wrappers = {}
  
  ox_elems.each do |elem|
    if elem.in
      wrappers[elem.in] ||= XML::Node.new elem.in
      elem.to_xml(data).each {|e| wrappers[elem.in] << e}
    else
      elem.to_xml(data).each {|e| ox << e}
    end
  end
  
  wrappers.each {|k,v| ox << v}
  
  ox_attrs.each do |a| 
    val = data.send(a.accessor).to_s
    ox[a.tag]= val if val.present?
  end
  
  ox
end