class Restfulie::Common::Builder::Marshalling::Atom < Restfulie::Common::Builder::Marshalling::Base include ActionController::UrlWriter include Restfulie::Common::Builder::Helpers include Restfulie::Common::Error ATOM_ATTRIBUTES = [ :title, :id, :updated, # Required :author, :links, # Recommended :category, :contributor, :rights # Optional ] ATOM_ATTRIBUTES_ENTRY = ATOM_ATTRIBUTES + [ :content, :summary, # Required :published, :source # Optional ] ATOM_ATTRIBUTES_FEED = ATOM_ATTRIBUTES + [ :generator, :icon, :logo, :subtitle # Optional ] ATTRIBUTES_ALREADY_IN_ATOM_SPEC = [ "id", "created_at", "updated_at", "title" ] def initialize(object, rules) @object = object @rules = rules end def builder_collection(options = {}) builder_feed(@object, @rules, options).to_xml end def builder_member(options = {}) builder_entry(@object, @rules, options).to_xml end private def builder_feed(objects, rules_blocks, options = {}) rule = Restfulie::Common::Builder::CollectionRule.new(rules_blocks) rule.blocks.unshift(default_collection_rule) if options[:default_rule] rule.metaclass.send(:attr_accessor, *ATOM_ATTRIBUTES_FEED) rule.apply(objects, options) atom = ::Atom::Feed.new do |feed| # Set values (ATOM_ATTRIBUTES_FEED - [:links]).each do |field| feed.send("#{field}=".to_sym, rule.send(field)) unless rule.send(field).nil? end # Namespaces builder_namespaces(rule.namespaces, feed) # Transitions rule.links.each do |link| atom_link = {:rel => link.rel, :href => link.href, :type => (link.type || 'application/atom+xml')} feed.links << ::Atom::Link.new(atom_link) unless atom_link[:href].nil? end # Entries options.delete(:values) member_options = options.merge(rule.members_options || {}) objects.each do |member| feed.entries << builder_entry(member, rule.members_blocks || [], member_options) end end end # TODO: Validate of the required fields def default_collection_rule Proc.new do |collection_rule, objects, options| # Passed values (options[:values] || {}).each { |key, value| collection_rule.send("#{key}=".to_sym, value)} # Default values collection_rule.id ||= options[:self] collection_rule.title ||= "#{objects.first.class.to_s.pluralize.demodulize} feed" collection_rule.updated ||= updated_collection(objects) # Transitions collection_rule.links << link(:rel => :self, :href => options[:self]) unless options[:self].nil? end end def builder_entry(object, rules_blocks, options = {}) rule = Restfulie::Common::Builder::MemberRule.new(rules_blocks) options = namespace_enhance(options) rule.blocks.unshift(default_member_rule) if options[:default_rule] rule.metaclass.send(:attr_accessor, *ATOM_ATTRIBUTES_ENTRY) rule.apply(object, options) atom = ::Atom::Entry.new do |entry| # Set values (ATOM_ATTRIBUTES_ENTRY - [:links]).each do |field| entry.send("#{field}=".to_sym, rule.send(field)) unless rule.send(field).nil? end # Namespaces builder_namespaces(rule.namespaces, entry) # Transitions rule.links.each do |link| atom_link = {:rel => link.rel, :href => link.href, :type => link.type} # Self if link.href.nil? if link.rel == "self" path = object else association = object.class.reflect_on_all_associations.find { |a| a.name.to_s == link.rel } path = (association.macro == :has_many) ? [object, association.name] : object.send(association.name) unless association.nil? end atom_link[:href] = polymorphic_url(path, :host => host) rescue nil atom_link[:type] = link.type || 'application/atom+xml' end entry.links << ::Atom::Link.new(atom_link) unless atom_link[:href].nil? end end end def default_member_rule Proc.new do |member_rule, object, options| # Passed values (options[:values] || {}).each { |key, value| set_attribute(member_rule, key, value) } # Default values member_rule.id ||= polymorphic_url(object, :host => host) rescue nil member_rule.title ||= object.respond_to?(:title) && !object.title.nil? ? object.title : "Entry about #{object.class.to_s.demodulize}" member_rule.updated ||= object.updated_at if object.respond_to?(:updated_at) # Namespace unless options[:namespace].nil? member_rule.namespace(object, options[:namespace][:uri], options[:namespace]) end end end def updated_collection(objects) objects.map { |item| item.updated_at if item.respond_to?(:updated_at) }.compact.max || Time.now end def builder_namespaces(namespaces, atom) kclass = atom.class namespaces.each do |ns| register_namespace(ns.namespace, ns.uri, kclass) ns.each do |key, value| unless ATTRIBUTES_ALREADY_IN_ATOM_SPEC.include?(key.to_s) register_element(ns.namespace, key, kclass) atom.send("#{ns.namespace}_#{key}=".to_sym, value) end end end end def host # TODO: If we split restfulie into 2 separate gems, we may need not to use Restfulie::Server # inside Restfulie::Common Restfulie::Server::Configuration.host end def register_namespace(namespace, uri, klass) klass.add_extension_namespace(namespace, uri) unless klass.known_namespaces.include? uri end def register_element(namespace, attribute, klass) attribute = "#{namespace}:#{attribute}" klass.element(attribute) if element_unregistered?(attribute, klass) end def element_unregistered?(element, klass) klass.element_specs.select { |k,v| v.name == element }.empty? end # TODO : Move error handling to class rule, maybe? def set_attribute(rule, attribute, value) begin rule.send("#{attribute}=".to_sym, value) rescue NoMethodError raise AtomMarshallingError.new("Attribute #{attribute} unsupported in Atom #{rule_type_name(rule)}.") end end def rule_type_name(rule) rule.kind_of?(Restfulie::Common::Builder::MemberRule) ? 'Entry' : 'Feed' end def namespace_enhance(options) if !options[:namespace].nil? && options[:namespace].kind_of?(String) options[:namespace] = { :uri => options[:namespace], :eager_load => true } end options end end