Class: Adaptation::Message

Inherits:
Object show all
Includes:
ROXML, Validateable
Defined in:
lib/adaptation/message.rb

Overview

Adaptation::Message – XML to classes mapping.

Adaptation::Message connects ruby objects and xml data to create a model where logic and data are presented in one wrapping. It’s is inspired on activerecord, and pretends to solve xml data management problems in a similar way activerecord handles database data management.

Adaptation::Message classes describe xml structures, allowing separation of common structures as standalone classes. It offers a set of macros to specify wich structures are part of a bigger one and to allow validation of the different parts.

A summary of the major features and their usage:

  • Automated mapping between classes and xml attributes and elements.

    class Contact < Adaptation::Message
      has_one :text, :name
    end
    
...is automatically mapped to a xml structure with root element named "contact", such as:

   <contact>
     <name>Name</name>
   </contact>

...which gives Contact#name
  • Associations between objects controlled by simple meta-programming macros.

    class Agenda < Adaptation::Message
      has_one  :attribute, :type
      has_one  :text, :owner
      has_many :contacts
    end
    
    class Contact < Adaptation::Message
      has_one :text, :name
    end
    
Agenda class would be mapping the following xml structure:

   <agenda type="...">
     <owner>...</owner>
     <contact>...</contact>
     <contact>...</contact>
     ...
   </agenda>

while the _Contact_ class would map:

   <contact>
     <name>...</name>
   </contact>

The Contact class is a partial, a structure included in a bigger structure, so its 
file name starts with an underscore: _contact.rb

We could have wrote Agenda like this, to change the contacts container name:

  class Agenda < Adaptation::Message
    has_one  :attribute, :type
    has_one  :text, :owner
    has_many :contacts, :in => :contact_list
  end

and Contact like:

  class Contact < Adaptation::Message
    has_one :text, :name
  end

Then the mapping in Agenda would be:

  <agenda type="...">
    <owner>...</owner>
    <contact_list>
      <contact>...</contact>
    </contact_list>
  </agenda>
  • Validation rules.

Adaptation::Message uses {ActiveRecord::Validations}[http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html].
This means that {ActiveRecord validations}[http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html]
can be used in an Adaptation::Message object and that custom validations can be easily implemented. 

Adaptation::Message also defines some {custom validation methods}[link:../rdoc/classes/ActiveRecord/Validations/ClassMethods.html].

Validations usage example:

  class Agenda < Adaptation::Message
    has_one :attribute, :type
    has_one :text, :owner
    validates_presence_of :owner, :type
    validates_value_of :type, "indexed"
  end

Constant Summary collapse

@@classes_with_brothers =
[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Validateable

append_features, included, #method_missing

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Validateable

Instance Attribute Details

#idObject (readonly)

avoid id method deprecation warnings



105
106
107
# File 'lib/adaptation/message.rb', line 105

def id
  @id
end

Class Method Details

.get_class_object(mapped_xml) ⇒ Object

:nodoc:



313
314
315
316
317
318
319
320
321
322
# File 'lib/adaptation/message.rb', line 313

def self.get_class_object(mapped_xml) #:nodoc:
  # TODO: reimplement this as read in ruby-talk (using 'inherited' method)
  mapped_xml = mapped_xml.downcase.to_sym if mapped_xml.is_a?(String)
  klass = nil
  ObjectSpace.each_object(Class) do |c|
    next unless c.ancestors.include?(Adaptation::Message) and (c != self) and (c != Adaptation::Message)
    (klass = c and break) if c.mapped_xml == mapped_xml rescue next
  end
  klass
end

.has_many(*options) ⇒ Object

Maps many xml subelements.

Example 1:

<xml>
  <subelement>...</subelement>
  <subelement>...</subelement>
  <subelement>...</subelement>
</xml>

class Xml < Adaptation::Message
  has_many :subelements
end

Example 2:

<xml>
  <subelement_list>
    <subelement>...</subelement>
    <subelement>...</subelement>
    <subelement>...</subelement>
  </subelement_list>
</xml>       

class Xml < Adaptation::Message
  has_many :subelements, :in => :subelement_list
end


244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/adaptation/message.rb', line 244

def self.has_many *options
  configuration = {}
  configuration.update(options.pop) if options.last.is_a?(Hash)
  
  class_with_container = nil
  if configuration[:in]
    class_with_container =  options[0].to_s.capitalize[0..-2] + ":#{configuration[:in].to_s}"
  else
    class_with_container =  options[0].to_s.capitalize[0..-2] + ":#{options[0].to_s}"  
  end    
  unless @@classes_with_brothers.include?(class_with_container)
    @@classes_with_brothers << class_with_container    
  end 
  
  @has_many = [] if @has_many.nil?
  unless @has_many.include?(options[0])
    @has_many << options[0]
    
    load "#{ADAPTOR_ROOT}/app/messages/_#{options[0].to_s[0..-2]}.rb"
    klass = get_class_object(options[0].to_s.capitalize[0..-2])
    
    if configuration[:in]
      xml_object configuration[:in], klass, ROXML::TAG_ARRAY, configuration[:in].to_s
      validates_associated configuration[:in]
    else
      xml_object options[0], klass, ROXML::TAG_ARRAY #, options[0].to_s 
      validates_associated options[0]
    end
  end
end

.has_one(*symbols) ⇒ Object

Maps an xml attribute or element.

Example 1: Mapping an attribute:

<xml attr="something"/>

class Xml < Adaptation::Message
  has_one :attribute, :attr
end

Example 2: Mapping an xml element that contains text.

<xml>
  <element>something</element>
</xml>

class Xml < Adaptation::Message
  has_one :text, :element
end

Example 3: Mapping an xml element that contains xml data.

<xml>
  <element>
    <subelement>something</subelement>
  </element>
</xml>

class Xml < Adaptation::Message
  has_one :object, :element
end

The element xml structure would be described in a separate partial file _element.rb:

class Element < Adaptation::Message
  has_one :text, :subelement
end


149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/adaptation/message.rb', line 149

def self.has_one *symbols
  xml_tag = symbols[0]
  case xml_tag
    when :object
      @objects = [] if @objects.nil?
      unless @objects.include?(symbols[1])
        @objects << symbols[1]

        klass = get_class_object(symbols[1])
        if klass.nil?
          require "#{ADAPTOR_ROOT}/app/messages/_#{symbols[1].to_s}.rb"
          klass = get_class_object(symbols[1])
        end

        xml_object symbols[1], klass
        validates_associated symbols[1]
      end
    when :attribute
      @attributes = [] if @attributes.nil?
      unless @attributes.include?(symbols[1])
        @attributes << symbols[1]
        s = ""
        @attributes.each do |a|
          s << a.to_s
        end
        xml_attribute symbols[1]
      end
    when :text
      @texts = [] if @texts.nil?
      unless @texts.include?(symbols[1])
        @texts << symbols[1]
        xml_text symbols[1]
      end
  end
end

.has_textObject

Maps an xml element text.

If an element contains plain text data and has not been declared in its parent element with has_one :text, it must declare itself with has_text to be able to contain text.

Example:

<xml>
  <element attr="something">something more</element>
</xml>

class Xml < Adaptation::Message
  has_one :object, :element
end

In this case element cannot be declared just like has_one :text, because it also has an attribute. It must be declared like has_one :object. Then the element declares itself in a file called _element.rb like this:

class Element < Adaptation::Message
  has_one :attribute, :attr
  has_text
end


209
210
211
212
213
214
# File 'lib/adaptation/message.rb', line 209

def self.has_text
  if @has_text.nil?
    @has_text = true
    xml_text :text, nil, ROXML::TEXT_CONTENT
  end
end

.mapped_xmlObject

Returns the xml element this class is mapping



309
310
311
# File 'lib/adaptation/message.rb', line 309

def self.mapped_xml
  @mapped_xml || self.to_s.downcase.gsub("::","_").to_sym
end

.maps_xml(element) ⇒ Object

Defines the xml element that this class is mapping. This is useful to avoid class name collisions:

Example:

We already have a database table called 'things' and we
interoperate with it with an ActiveRecord subclass called
'Thing':

  class Thing < ActiveRecord::Base
    ...
  end

But in the same Adaptation application we want to parse the
following xml:

  <thing>...</thing>

Defining another class Thing would produce a class name 
collision, but we can do:

  class XmlThing < Adaptation::Message
    maps_xml :thing
    ...     
  end

and store it in a file called app/messages/thing.rb


303
304
305
306
# File 'lib/adaptation/message.rb', line 303

def self.maps_xml element
  @mapped_xml = element
  xml_name element.to_s
end

.to_object(xml_message) ⇒ Object

This is the constructor. Instead of Adaptation::Message#new, an Adaptation::Message instance is created like this:

m = Adaptation::Message.to_object("<xml>valid xml</xml>")


329
330
331
# File 'lib/adaptation/message.rb', line 329

def self.to_object xml_message
  parse xml_message
end

Instance Method Details

#checkObject

Alias for Adaptation::Message#valid? Deprecated, use valid? instead.



335
336
337
# File 'lib/adaptation/message.rb', line 335

def check
  valid?
end