lib/atom/element.rb in atom-tools-1.0.0 vs lib/atom/element.rb in atom-tools-2.0.0

- old
+ new

@@ -1,286 +1,640 @@ require "time" require "rexml/element" +require 'uri' + +module URI # :nodoc: all + class Generic; def to_uri; self; end; end +end + +class String # :nodoc: + def to_uri; URI.parse(self); end +end + +# cribbed from metaid.rb +class Object + # The hidden singleton lurks behind everyone + def metaclass; class << self; self; end; end + def meta_eval &blk; metaclass.instance_eval &blk; end + + # Adds methods to a metaclass + def meta_def name, &blk + meta_eval { define_method name, &blk } + end +end + module Atom # :nodoc: - class Time < ::Time # :nodoc: - def self.new date - return if date.nil? + NS = "http://www.w3.org/2005/Atom" + PP_NS = "http://www.w3.org/2007/app" - date = if date.respond_to?(:iso8601) - date - else - Time.parse date.to_s - end - - def date.to_s - iso8601 - end + class ParseError < StandardError; end - date - end + module AttrEl + # for backwards compatibility + def [] k; self.send(k.to_sym); end + def []= k, v; self.send("#{k}=".to_sym, v); end end - + # ignore the man behind the curtain. def self.Multiple klass Class.new(Array) do @class = klass - def new - item = self.class.holds.new + def new *args + item = self.class.holds.new *args self << item - + item end def << item raise ArgumentError, "this can only hold items of class #{self.class.holds}" unless item.is_a? self.class.holds + super(item) end - def to_element - collect do |item| item.to_element end - end - def self.holds; @class end def self.single?; true end def taguri; end end end - # The Class' methods provide a DSL for describing Atom's structure - # (and more generally for describing simple namespaced XML) - class Element < Hash - # a REXML::Element that shares this element's extension attributes - # and child elements - attr_reader :extensions + module Parsers + # adds a parser that calls the given block for a single element that + # matches the given name and namespace (if it exists) + def on_parse name_pair, &block + uri, name = name_pair + @on_parse ||= [] - # this element's xml:base - attr_accessor :base + process = lambda do |e,x| + el = e.get_elem(x, uri, name) - # this element's attributes - def self.attrs # :nodoc: - @attrs || [] + if el + block.call e, el + + e.extensions.delete_if do |c| + c.namespace == uri and c.name == name.to_s + end + end + end + + @on_parse << process end - # this element's child elements - def self.elements # :nodoc: - @elements || [] + # adds a parser that calls the given block for the attribute that + # matches the given name (if it exists) + def on_parse_attr name_pair, &block + uri, name = name_pair + @on_parse ||= [] + + process = lambda do |e,x| + x = e.get_atom_attrb(x, name) + + if x + block.call e, x + + e.extensions.attributes.delete name.to_s + end + end + + @on_parse << process end - # required child elements - def self.required # :nodoc: - @elements.find { |name,kind,req| req } + # adds a parser that calls the given block for all elements + # that match the given name and namespace + def on_parse_many name_pair, &block + uri, name = name_pair + @on_parse ||= [] + + process = lambda do |e,x| + els = e.get_elems(x, uri, name) + + unless els.empty? + block.call e, els + + els.each do |el| + e.extensions.delete_if { |c| c.namespace == uri and c.name == name.to_s } + end + end + end + + @on_parse << process end - # copy defined elements and attributes so inheritance works - def self.inherited klass # :nodoc: - elements.each do |name, kind, req| - klass.element name, kind, req + # adds a parser that calls the given block for this element + def on_parse_root &block + @on_parse ||= [] + + process = lambda do |e,x| + block.call e, x + + x.elements.each do |el| + e.extensions.clear + end end - attrs.each do |name, req| - klass.attrb name, req + + @on_parse << process + end + + # parses the text content of an element named 'name' into an attribute + # on this Element named 'name' + def parse_plain uri, name + self.on_parse [uri, name] do |e,x| + e.set(name, x.text) end end + end - # define a child element - def self.element(name, kind, req = false) # :nodoc: + module Converters + def build_plain ns, name + self.on_build do |e,x| + if v = e.get(name) + el = e.append_elem(x, ns, name) + el.text = v.to_s + end + end + end + + # an element in the Atom namespace containing text + def atom_string(name) + attr_accessor name + + self.parse_plain(Atom::NS, name) + self.build_plain(['atom', Atom::NS], name) + end + + # an element in namespace 'ns' containing a RFC3339 timestamp + def time(ns, name) attr_reader name - @elements ||= [] - @elements << [name, kind, req] + self.def_set name do |time| + unless time.respond_to? :iso8601 + time = Time.parse(time.to_s) + end - unless kind.respond_to? :single? - self.define_accessor(name,kind) + def time.to_s; iso8601; end + + instance_variable_set("@#{name}", time) end + + define_method "#{name}!".to_sym do + set(name, Time.now) + end + + self.parse_plain(ns[1], name) + self.build_plain(ns, name) end - # define an attribute - def self.attrb(name, req = false) # :nodoc: - @attrs ||= [] + # an element in the Atom namespace containing a timestamp + def atom_time(name) + self.time ['atom', Atom::NS], name + end - @attrs << [name, req] + # an element that is parsed by Element descendant 'klass' + def element(ns, name, klass) + el_name = name + name = name.to_s.gsub(/-/, '_') + + attr_reader name + + self.on_parse [ns[1], el_name] do |e,x| + e.instance_variable_set("@#{name}", klass.parse(x, e.base)) + end + + self.on_build do |e,x| + if v = e.get(name) + el = e.append_elem(x, ns, el_name) + v.build(el) + end + end + + def_set name do |value| + instance_variable_set("@#{name}", klass.new(value)) + end end - - # a little bit of magic - def self.define_accessor(name,kind) # :nodoc: - define_method "#{name}=".to_sym do |value| - return unless value - - i = if kind.ancestors.member? Atom::Element - kind.new(value, name.to_s) - else - kind.new(value) + + # an element that is parsed by Element descendant 'klass' + def atom_element(name, klass) + self.element(['atom', Atom::NS], name, klass) + end + + # an element that can appear multiple times that contains text + # + # 'one_name' is the name of the element, 'many_name' is the name of + # the attribute that will be created on this Element + def strings(ns, one_name, many_name) + attr_reader many_name + + self.on_init do + instance_variable_set("@#{many_name}", []) + end + + self.on_parse_many [ns[1], one_name] do |e,xs| + var = e.instance_variable_get("@#{many_name}") + + xs.each do |el| + var << el.text end - - set(name, i) end + + self.on_build do |e,x| + e.instance_variable_get("@#{many_name}").each do |v| + e.append_elem(x, ns, one_name).text = v + end + end end - # get the value of an attribute - def [] key - test_key key - - super + # an element that can appear multiple times that is parsed by Element + # descendant 'klass' + # + # 'one_name' is the name of the element, 'many_name' is the name of + # the attribute that will be created on this Element + def elements(ns, one_name, many_name, klass) + attr_reader many_name + + self.on_init do + var = Atom::Multiple(klass).new + instance_variable_set("@#{many_name}", var) + end + + self.on_parse_many [ns[1], one_name] do |e,xs| + var = e.get(many_name) + + xs.each do |el| + var << klass.parse(el, e.base) + end + end + + self.on_build do |e,x| + e.get(many_name).each do |v| + el = e.append_elem(x, ns, one_name) + v.build(el) + end + end end - - # set the value of an attribute - def []= key, value - test_key key - super + # like #elements but in the Atom namespace + def atom_elements(one_name, many_name, klass) + self.elements(['atom', Atom::NS], one_name, many_name, klass) end - # internal junk you probably don't care about - def initialize name = nil # :nodoc: - @extensions = REXML::Element.new("extensions") - @local_name = name + # an XML attribute in the namespace 'ns' + def attrb(ns, name) + attr_accessor name - self.class.elements.each do |name,kind,req| - if kind.respond_to? :single? - a = kind.new - set(name, kind.new) + self.on_parse_attr [ns[1], name] do |e,x| + e.set(name, x) + end + + self.on_build do |e,x| + if v = e.get(name) + e.set_atom_attrb(x, name, v.to_s) end end end - # eg. "feed" or "entry" or "updated" or "title" or ... - def local_name # :nodoc: - @local_name || self.class.name.split("::").last.downcase + # an XML attribute in the Atom namespace + def atom_attrb(name) + self.attrb(['atom', Atom::NS], name) end - - # convert to a REXML::Element (with no namespace) - def to_element - elem = REXML::Element.new(local_name) - self.class.elements.each do |name,kind,req| - v = get(name) - next if v.nil? + # a type of Atom Link. specifics defined by Hash 'criteria' + def atom_link name, criteria + def_get name do + existing = find_link(criteria) - if v.respond_to? :to_element - e = v.to_element - e = [ e ] unless e.is_a? Array + existing and existing.href + end - e.each do |bit| - elem << bit - end + def_set name do |value| + existing = find_link(criteria) + + if existing + existing.href = value else - e = REXML::Element.new(name.to_s, elem).text = get(name) + links.new criteria.merge(:href => value) end end + end + end - self.class.attrs.each do |name,req| - value = self[name.to_s] - elem.attributes[name.to_s] = value.to_s if value + # The Class' methods provide a DSL for describing Atom's structure + # (and more generally for describing simple namespaced XML) + class Element + # this element's xml:base + attr_accessor :base + + # xml elements and attributes that have been parsed, but are unknown + attr_reader :extensions + + # attaches a name and a namespace to an element + # this needs to be called on any new element + def self.is_element ns, name + meta_def :self_namespace do; ns; end + meta_def :self_name do; name.to_s; end + end + + # wrapper for #is_element + def self.is_atom_element name + self.is_element Atom::NS, name + end + + # gets a single namespaced child element + def get_elem xml, ns, name + REXML::XPath.first xml, "./ns:#{name}", { 'ns' => ns } + end + + # gets multiple namespaced child elements + def get_elems xml, ns, name + REXML::XPath.match xml, "./ns:#{name}", { 'ns' => ns } + end + + # gets a child element in the Atom namespace + def get_atom_elem xml, name + get_elem xml, Atom::NS, name + end + + # gets multiple child elements in the Atom namespace + def get_atom_elems xml, name + get_elems Atom::NS, name + end + + # gets an attribute on +xml+ + def get_atom_attrb xml, name + xml.attributes[name.to_s] + end + + # sets an attribute on +xml+ + def set_atom_attrb xml, name, value + xml.attributes[name.to_s] = value + end + + extend Parsers + extend Converters + + def self.on_build &block + @on_build ||= [] + @on_build << block + end + + def self.do_parsing e, root + if ancestors[1].respond_to? :do_parsing + ancestors[1].do_parsing e, root end - self.extensions.children.each do |element| - elem << element.dup # otherwise they get removed from @extensions + @on_parse ||= [] + @on_parse.each { |p| p.call e, root } + end + + def self.builders &block + if ancestors[1].respond_to? :builders + ancestors[1].builders &block end - if self.base and not self.base.empty? - elem.attributes["xml:base"] = self.base + @on_build ||= [] + @on_build.each &block + end + + # turns a String, an IO-like, a REXML::Element, etc. into an Atom::Element + # + # the 'base' base URL parameter should be supplied if you know where this + # XML was fetched from + # + # if you want to parse into an existing Atom::Element, it can be passed in + # as 'element' + def self.parse xml, base = '', element = nil + if xml.respond_to? :elements + root = xml.dup + else + xml = xml.read if xml.respond_to? :read + + begin + root = REXML::Document.new(xml.to_s).root + rescue REXML::ParseException => e + raise Atom::ParseError, e.message + end + end + + unless root.local_name == self.self_name + raise Atom::ParseError, "expected element named #{self.self_name}, not #{root.local_name}" end - elem + unless root.namespace == self.self_namespace + raise Atom::ParseError, "expected element in namespace #{self.self_namespace}, not #{root.namespace}" + end + + if root.attributes['xml:base'] + base = (base.to_uri + root.attributes['xml:base']) + end + + e = element ? element : self.new + e.base = base + + # extension elements + root.elements.each do |c| + e.extensions << c + end + + # extension attributes + root.attributes.each do |k,v| + e.extensions.attributes[k] = v + end + + # as things are parsed, they're removed from e.extensions. whatever's + # left over is stored so it can be round-tripped + + self.do_parsing e, root + + e end - - # convert to a REXML::Document (properly namespaced) + + # converts to a REXML::Element def to_xml - doc = REXML::Document.new - root = to_element - root.add_namespace Atom::NS - doc << root - doc + root = REXML::Element.new self.class.self_name + root.add_namespace self.class.self_namespace + + build root + + root end - - # convert to an XML string + + # fill a REXML::Element with the data from this Atom::Element + def build root + if self.base and not self.base.empty? + root.attributes['xml:base'] = self.base + end + + self.class.builders do |builder| + builder.call self, root + end + + @extensions.each do |e| + root << e.dup + end + + @extensions.attributes.each do |k,v| + root.attributes[k] = v + end + end + def to_s to_xml.to_s end - - def base= uri # :nodoc: - @base = uri.to_s + + # defines a getter that calls 'block' + def self.def_get(name, &block) + define_method name.to_sym, &block end - - private - # like +valid_key?+ but raises on failure - def test_key key - unless valid_key? key - raise RuntimeError, "this element (#{local_name}) doesn't have that attribute '#{key}'" + # defines a setter that calls 'block' + def self.def_set(name, &block) + define_method "#{name}=".to_sym, &block + end + + # be sure to call #super if you override this method! + def initialize defaults = {} + @extensions = [] + + @extensions.instance_variable_set('@attrs', {}) + def @extensions.attributes + @attrs end + + self.class.initters do |init| + self.instance_eval &init + end + + defaults.each do |k,v| + set(k, v) + end end - # tests that an attribute 'key' has been defined - def valid_key? key - self.class.attrs.find { |name,req| name.to_s == key } + def self.on_init &block + @on_init ||= [] + @on_init << block end + def self.initters &block + @on_init ||= [] + @on_init.each &block + end + + # appends an element named 'name' in namespace 'ns' to 'root' + # ns is either [prefix, namespace] or just a String containing the namespace + def append_elem(root, ns, name) + if ns.is_a? Array + prefix, uri = ns + else + prefix, uri = nil, ns + end + + name = name.to_s + + existing_prefix = root.namespaces.find do |k,v| + v == uri + end + + root << if existing_prefix + prefix = existing_prefix[0] + + if prefix != 'xmlns' + name = prefix + ':' + name + end + + REXML::Element.new(name) + elsif prefix + e = REXML::Element.new(prefix + ':' + name) + e.add_namespace(prefix, uri) + e + else + e = REXML::Element.new(name) + e.add_namespace(uri) + e + end + end + + def base= uri # :nodoc: + @base = uri.to_s + end + + # calls a getter def get name - instance_variable_get "@#{name}" + send "#{name}".to_sym end + # calls a setter def set name, value - instance_variable_set "@#{name}", value + send "#{name}=", value end end - - # this facilitates YAML output - class AttrEl < Atom::Element # :nodoc: - end # A link has the following attributes: # # href (required):: the link's IRI # rel:: the relationship of the linked item to the current item # type:: a hint about the media type of the linked item # hreflang:: the language of the linked item (RFC3066) # title:: human-readable information about the link # length:: a hint about the length (in octets) of the linked item - class Link < Atom::AttrEl - attrb :href, true - attrb :rel - attrb :type - attrb :hreflang - attrb :title - attrb :length + class Link < Atom::Element + is_atom_element :link - def initialize name = nil # :nodoc: - super name + atom_attrb :href + atom_attrb :rel + atom_attrb :type + atom_attrb :hreflang + atom_attrb :title + atom_attrb :length - # just setting a default - self["rel"] = "alternate" + include AttrEl + + def rel + @rel or 'alternate' end + + def self.parse xml, base = '' + e = super + + # URL absolutization + if e.base and e.href + e.href = (e.base.to_uri + e.href).to_s + end + + e + end end - + # A category has the following attributes: # # term (required):: a string that identifies the category # scheme:: an IRI that identifies a categorization scheme # label:: a human-readable label - class Category < Atom::AttrEl - attrb :term, true - attrb :scheme - attrb :label + class Category < Atom::Element + is_atom_element :category + + atom_attrb :term + atom_attrb :scheme + atom_attrb :label + + include AttrEl end # A person construct has the following child elements: # # name (required):: a human-readable name # uri:: an IRI associated with the person # email:: an email address associated with the person - class Author < Atom::Element - element :name, String, true - element :uri, String - element :email, String + class Person < Atom::Element + atom_string :name + atom_string :uri + atom_string :email end - - # same as Atom::Author - class Contributor < Atom::Element - # Author and Contributor should probably inherit from Person, but - # oh well. - element :name, String, true - element :uri, String - element :email, String + + class Author < Atom::Person + is_atom_element :author + end + + class Contributor < Atom::Person + is_atom_element :contributor end end