Proposed Solutions

The following are ideas for solutions.  Some represent just ideas that can be modified or parts taken from.  The final solution could be parts take from each of these.

ActiveFedora

I think you have two basic problems:

  1. By default, AF assumes multivalued fields and returns an array for a given value of a field.  If the field is designed as :unique, AF returns a string.  Working with multivalued fields mean you have to deal with these arrays creating, updating and deleting values from them.  Here's some sample rspec code that illustrates the issues:

    1  obj = SampleActiveFedoraModel.new
    2  obj.field = "foo"
    3  obj.field.should == ["foo"]
    4  obj.field = ["foo", "bar"]
    5  obj.field.should == ["foo", "bar"]
    6  obj.field = ["bar"]
    7  obj.field.should == ["bar"] # Returns FALSE! Actual value is ["bar", "bar"]
    8  obj.field = ["", "bar"]
    9  obj.field.should == ["bar"] # Returns true

     

    1. In line 8, deleting a term involves passing an empty string with the same index.  So, how do you maintain indexes in your view so you know which term you're updating or deleting?
    2. Is there a way to write nice view and controller code without looping through arrays of terms?
    3. Other solutions such as making line 7 return true?  How does that affect more complicated scenarios with more than two terms: does the order of terms determine everything?
  2. Multiple fields require xml nodes to be added to the existing xml
    1. Depending on the term, OM can sometimes add the additional xml required for a term, but not always
    2. For complex, nested xml terms, you must resort to inserting additional nodes into the document

Inserting Nodes

Using OM, there's already a pretty good way of defining templates for inserted nodes and getting them into your document.  The trick is implementing that at the AF-level.  For example, define your xml in OM or other, and include a template as well as an additional method for inserting and removing that template:

class FakeMods < ActiveFedora::NokogiriDatastream
 
  set_terminology do |t|
    t.root(:path=>"mods")
    
    t.contributor do
      t.author
      t.role
    end
    
    t.contributor_name(:proxy=>[:contributor, :name], :index_as => [:searchable])
    t.contributor_role(:proxy=>[:contributor, :role], :index_as => [:searchable])
  end
 
  define_template :contributor do |xml, person, relator|
    xml.contributor {
      xml.name(person)
      xml.role(relator, :source=>"MARC relator terms")
    }
  end
 
  def insert_contributor(person=nil, relator=nil)
    add_child_node(ng_xml.root, :contributor, person, relator)
  end
 
  def remove_contributor(index)
    self.find_by_terms(:contributor).slice(index.to_i).remove
  end
 
end

However, the above only works at the datastream level, and not at the object level.  It might be useful to use AF to push template functions from a given to datastream up to the object level, much in the same way that the delegate functions work.  An example could look something like this:

class MyModel < ActiveFedora::Base
 
  has_metadata :name => "descMetadata", :type => FakeMods
 
  delegate_to :descMetadata, [:contributor_name, :contributor_role]
  delegate :contributor, :to => :descMetadata, :template => true
 
end

You could now add new contributor nodes by calling the methods on the object.  The :template option used in the model would add insert_ and delete_ methods, then getters and setters would be the existing OM terms.  However, the issue of arrays, as explained above, would still remain.  Here's some same console code to illustrate usage:

> obj = MyModel.new
> obj.insert_contributor("John Doe", "author")
> obj.insert_contributor("Jane Doe", "illustrator")
> obj.contributor_name
=> ["John Doe", "Jane Doe"]
> obj.contributor_role
=> ["author","illustrator"]
> obj.contributor_name = ["Doe, John", "Doe, Jane"]
> obj.contributor_name
=> ["Doe, John", "Doe, Jane"]
> obj.delete_contributor(0)
> obj.contributor_name
=> ["Doe, Jane"]

This just moves the interaction of node insertion and updating to the object level, making it a bit easier for controllers in interact with the data.  However, it doesn't solve the problem of indexes, which still have to be maintained when deleting, nor does it solve the issue of arrays and maintaining their index positions.

Handled_By flag in terminology

class MyDatastream < ActiveFedora::NokogiriDatastream
  set_terminology do |t|
    t.related_item, :path => "relatedItem", :handled_by => RelatedItem
  end
end

Then RelatedItem can be a class that could provide a method that ActiveFedora will use to map the URL parameters to XML (and vice-versa).

class RelatedItem

  def params_from_xml(xml)
    # Maybe use OM here, or just straight Nokogiri and XPath to map each path in the node to a class attribute.
    # This is the most un-thought out part of this pseudo code and might be the major downfall to this approach.
  end

  def self.xml_from_params(related_items)
    Nokogiri::XML::Builder.new do |xml|
      related_items.each do |item|
        xml.relatedItem {
            xml.titleInfo {
              xml.title{item[:title]}
            }
            xml.location {
              xml.url {item[:url]}
            }
        }
      end
    end
  end

end

The View would look like this (and I believe would not need to have indexes maintained).

<%= text_field :"related_item[][title]" %>
<%= text_field :"related_item[][url]" %>

Which should generate this into the params hash: "related_item" => [ {:title => "Title1", :url => "google.com"} , {:title => "Title2" , :url => "stanford.edu"} ]

Those params are then sent into the xml_from_params class method of the class referenced in the :handled_by in the set_terminology block which will then return XML to be added to the datastream.