From e289fcf81a22f1dc6ebcb87861877b463af04b7c Mon Sep 17 00:00:00 2001 From: kou Date: Tue, 6 Jul 2004 17:43:05 +0000 Subject: [PATCH] * lib/rss/{rss,parser,0.9,1.0,2.0}.rb: supported RSS 0.9x/2.0 validation and validation which disregard order of elements. * test/rss/test_parser.rb: added tests for RSS 0.9x/2.0 validation. * test/rss/{test_trackback,rss-testcase}.rb: fixed no good method name. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@6590 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 9 ++ lib/rss/0.9.rb | 166 +++++++++++++++++++++++++++------ lib/rss/1.0.rb | 7 ++ lib/rss/2.0.rb | 81 ++++++++-------- lib/rss/parser.rb | 32 +++++-- lib/rss/rss.rb | 28 +++++- test/rss/rss-testcase.rb | 35 ++++++- test/rss/test_parser.rb | 183 +++++++++++++++++++++++++++++++++---- test/rss/test_trackback.rb | 18 ++-- 9 files changed, 446 insertions(+), 113 deletions(-) diff --git a/ChangeLog b/ChangeLog index d77cb3e272..9d1122f099 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +Wed Jul 7 02:31:41 2004 Kouhei Sutou + + * lib/rss/{rss,parser,0.9,1.0,2.0}.rb: supported RSS 0.9x/2.0 + validation and validation which disregard order of elements. + * test/rss/test_parser.rb: added tests for RSS 0.9x/2.0 + validation. + * test/rss/{test_trackback,rss-testcase}.rb: fixed no good method + name. + Wed Jul 7 00:48:34 2004 WATANABE Hirofumi * ext/tk/lib/tkextlib/tktrans.rb, diff --git a/lib/rss/0.9.rb b/lib/rss/0.9.rb index bb3cc23beb..c95c47e974 100644 --- a/lib/rss/0.9.rb +++ b/lib/rss/0.9.rb @@ -5,6 +5,12 @@ module RSS module RSS09 NSPOOL = {} ELEMENTS = [] + + def self.append_features(klass) + super + + klass.install_must_call_validator('', nil) + end end class Rss < Element @@ -62,22 +68,34 @@ EOR [@channel] end + def _tags + [ + [nil, 'channel'], + ].delete_if {|x| send(x[1]).nil?} + end + + def _attrs + [ + ["version", true], + ] + end + class Channel < Element include RSS09 [ ["title", nil], - ["link", nil], - ["description", nil], - ["language", nil], - ["copyright", "?"], - ["managingEditor", "?"], - ["webMaster", "?"], - ["rating", "?"], - ["docs", "?"], - ["skipDays", "?"], - ["skipHours", "?"], + ["link", nil], + ["description", nil], + ["language", nil], + ["copyright", "?"], + ["managingEditor", "?"], + ["webMaster", "?"], + ["rating", "?"], + ["docs", "?"], + ["skipDays", "?"], + ["skipHours", "?"], ].each do |x, occurs| install_text_element(x) install_model(x, occurs) @@ -85,7 +103,7 @@ EOR [ ["pubDate", "?"], - ["lastBuildDate", "?"], + ["lastBuildDate", "?"], ].each do |x, occurs| install_date_element(x, 'rfc822') install_model(x, occurs) @@ -93,13 +111,19 @@ EOR [ ["image", nil], - ["textInput", "?"], - ["cloud", "?"] + ["textInput", "?"], ].each do |x, occurs| install_have_child_element(x) install_model(x, occurs) end + [ + ["cloud", "?"] + ].each do |x, occurs| + install_have_attribute_element(x) + install_model(x, occurs) + end + [ ["item", "*"] ].each do |x, occurs| @@ -142,12 +166,46 @@ EOT [@image, @textInput, @cloud, *@item] end + def _tags + rv = [ + "title", + "link", + "description", + "language", + "copyright", + "managingEditor", + "webMaster", + "rating", + "docs", + "skipDays", + "skipHours", + "image", + "textInput", + "cloud", + ].delete_if do |x| + send(x).nil? + end.collect do |elem| + [nil, elem] + end + + @item.each do + rv << [nil, "item"] + end + + rv + end + class Image < Element include RSS09 - %w(url title link width height description).each do |x| + %w(url title link).each do |x| install_text_element(x) + install_model(x, nil) + end + %w(width height description).each do |x| + install_text_element(x) + install_model(x, "?") end def to_s(convert=true) @@ -166,6 +224,14 @@ EOT rv end + private + def _tags + %w(url title link width height description).delete_if do |x| + send(x).nil? + end.collect do |elem| + [nil, elem] + end + end end class Cloud < Element @@ -173,15 +239,24 @@ EOT include RSS09 [ - ["domain", nil, false], - ["port", nil, false], - ["path", nil, false], - ["registerProcedure", nil, false], - ["protocol", nil ,false], + ["domain", nil, true], + ["port", nil, true], + ["path", nil, true], + ["registerProcedure", nil, true], + ["protocol", nil ,true], ].each do |name, uri, required| install_get_attribute(name, uri, required) end + def initialize(domain, port, path, rp, protocol) + super() + @domain = domain + @port = port + @path = path + @registerProcedure = rp + @protocol = protocol + end + def to_s(convert=true) rv = <<-EOT EOT @@ -240,6 +318,20 @@ EOT rv end + private + def children + [@category, @source, @enclosure,].compact + end + + def _tags + %w(title link description author comments category + source enclosure).delete_if do |x| + send(x).nil? + end.collect do |x| + [nil, x] + end + end + class Source < Element include RSS09 @@ -249,7 +341,7 @@ EOT ].each do |name, uri, required| install_get_attribute(name, uri, required) end - + content_setup def initialize(url=nil, content=nil) @@ -270,6 +362,10 @@ EOT end private + def _tags + [] + end + def _attrs [ ["url", true] @@ -284,8 +380,8 @@ EOT [ ["url", nil, true], - ["length", nil, true], - ["type", nil, true], + ["length", nil, true], + ["type", nil, true], ].each do |name, uri, required| install_get_attribute(name, uri, required) end @@ -312,8 +408,8 @@ EOT def _attrs [ ["url", true], - ["length", true], - ["type", true], + ["length", true], + ["type", true], ] end @@ -365,6 +461,7 @@ EOT %w(title description name link).each do |x| install_text_element(x) + install_model(x, nil) end def to_s(convert=true) @@ -381,6 +478,14 @@ EOT rv end + private + def _tags + %w(title description name link).each do |x| + send(x).nil? + end.collect do |elem| + [nil, elem] + end + end end end @@ -397,6 +502,7 @@ EOT check_ns(tag_name, prefix, ns, nil) @rss = Rss.new(attrs['version'], @version, @encoding, @standalone) + @rss.do_validate = @do_validate @rss.xml_stylesheets = @xml_stylesheets @last_element = @rss @proc_stack.push Proc.new { |text, tags| diff --git a/lib/rss/1.0.rb b/lib/rss/1.0.rb index 696b492ada..be141d73d8 100644 --- a/lib/rss/1.0.rb +++ b/lib/rss/1.0.rb @@ -5,6 +5,13 @@ module RSS module RSS10 NSPOOL = {} ELEMENTS = [] + + def self.append_features(klass) + super + + klass.install_must_call_validator('', ::RSS::URI) + end + end class RDF < Element diff --git a/lib/rss/2.0.rb b/lib/rss/2.0.rb index c83fb2c393..c6c6db58a7 100644 --- a/lib/rss/2.0.rb +++ b/lib/rss/2.0.rb @@ -4,30 +4,21 @@ module RSS class Rss -# URI = "http://backend.userland.com/rss2" - -# install_ns('', URI) - -# def self.required_uri -# URI -# end - class Channel -# def self.required_uri -# URI -# end - %w(generator ttl).each do |x| install_text_element(x) + install_model(x, '?') end %w(category).each do |x| install_have_child_element(x) + install_model(x, '?') end [ ["image", "?"], + ["language", "?"], ].each do |x, occurs| install_model(x, occurs) end @@ -40,17 +31,33 @@ module RSS EOT rv << super end + + private + alias children09 children + def children + children09 + [@category].compact + end + + alias _tags09 _tags + def _tags + %w(generator ttl category).delete_if do |x| + send(x).nil? + end.collect do |elem| + [nil, elem] + end + _tags09 + end Category = Item::Category -# def Category.required_uri -# URI -# end class Item -# def self.required_uri -# URI -# end + [ + ["comments", "?"], + ["author", "?"], + ].each do |x, occurs| + install_text_element(x) + install_model(x, occurs) + end [ ["pubDate", '?'], @@ -68,20 +75,33 @@ EOT def other_element(convert, indent='') rv = <<-EOT +#{indent}#{author_element(false)} +#{indent}#{comments_element(false)} #{indent}#{pubDate_element(false)} #{indent}#{guid_element(false)} EOT rv << super end + private + alias children09 children + def children + children09 + [@guid].compact + end + + alias _tags09 _tags + def _tags + %w(comments author pubDate guid).delete_if do |x| + send(x).nil? + end.collect do |elem| + [nil, elem] + end + _tags09 + end + class Guid < Element include RSS09 -# def self.required_uri -# URI -# end - [ ["isPermaLink", nil, false] ].each do |name, uri, required| @@ -124,24 +144,7 @@ EOT end RSS09::ELEMENTS.each do |x| -# BaseListener.install_get_text_element(x, Rss::URI, "#{x}=") BaseListener.install_get_text_element(x, nil, "#{x}=") end - module ListenerMixin - private - alias start_rss09 start_rss - def start_rss(tag_name, prefix, attrs, ns) -# check_ns(tag_name, prefix, ns, Rss::URI) - - @rss = Rss.new(attrs['version'], @version, @encoding, @standalone) - @rss.xml_stylesheets = @xml_stylesheets - @last_element = @rss - @proc_stack.push Proc.new { |text, tags| - @rss.validate_for_stream(tags) if @do_validate - } - end - - end - end diff --git a/lib/rss/parser.rb b/lib/rss/parser.rb index e1c61f46ae..1a8fdd4930 100644 --- a/lib/rss/parser.rb +++ b/lib/rss/parser.rb @@ -124,11 +124,22 @@ module RSS class << self @@setter = {} + @@registered_uris = {} + def install_setter(uri, tag_name, setter) @@setter[uri] = {} unless @@setter.has_key?(uri) @@setter[uri][tag_name] = setter end + def register_uri(name, uri) + @@registered_uris[name] = {} unless @@registered_uris.has_key?(name) + @@registered_uris[name][uri] = nil + end + + def uri_registered?(name, uri) + @@registered_uris[name].has_key?(uri) + end + def setter(uri, tag_name) begin @@setter[uri][tag_name] @@ -147,27 +158,32 @@ module RSS def install_get_text_element(name, uri, setter) install_setter(uri, name, setter) - def_get_text_element(name, *get_file_and_line_from_caller(1)) + def_get_text_element(uri, name, *get_file_and_line_from_caller(1)) end private - def def_get_text_element(name, file, line) + def def_get_text_element(uri, name, file, line) + register_uri(name, uri) unless private_instance_methods(false).include?("start_#{name}") module_eval(<<-EOT, file, line) def start_#{name}(name, prefix, attrs, ns) uri = ns[prefix] - if @do_validate - tags = self.class.available_tags(uri) - unless tags.include?(name) - raise UnknownTagError.new(name, uri) + if self.class.uri_registered?(#{name.inspect}, uri) + if @do_validate + tags = self.class.available_tags(uri) + unless tags.include?(name) + raise UnknownTagError.new(name, uri) + end end + start_get_text_element(name, prefix, ns, uri) + else + start_else_element(name, prefix, attrs, ns) end - start_get_text_element(name, prefix, ns, uri) end EOT + send("private", "start_#{name}") end - send("private", "start_#{name}") end end diff --git a/lib/rss/rss.rb b/lib/rss/rss.rb index 9c616238fe..b06cf68d81 100644 --- a/lib/rss/rss.rb +++ b/lib/rss/rss.rb @@ -35,6 +35,20 @@ class Time end end +module Enumerable + unless instance_methods.include?("sort_by") + def sort_by + collect do |x| + [yield(x), x] + end.sort do |x, y| + x[0] <=> y[0] + end.collect! do |x| + x[1] + end + end + end +end + require "English" require "rss/utils" require "rss/converter" @@ -42,7 +56,9 @@ require "rss/xml-stylesheet" module RSS - VERSION = "0.0.8" + VERSION = "0.0.9" + + URI = "http://purl.org/rss/1.0/" DEBUG = false @@ -298,8 +314,6 @@ EOC end - URI = "http://purl.org/rss/1.0/" - class Element extend BaseModel @@ -314,7 +328,7 @@ EOC TAG_NAME = name.split('::').last.downcase - @@must_call_validators = {::RSS::URI => ''} + @@must_call_validators = {} def self.must_call_validators @@must_call_validators @@ -427,6 +441,7 @@ EOC end def validate_for_stream(tags) + validate_attribute __validate(tags, false) end @@ -504,6 +519,11 @@ EOC do_redo = false not_shift = false tag = nil + element_names = model.collect {|elem| elem[0]} + if tags + tags_size = tags.size + tags = tags.sort_by {|x| element_names.index(x) || tags_size} + end model.each_with_index do |elem, i| diff --git a/test/rss/rss-testcase.rb b/test/rss/rss-testcase.rb index 349e3d4dc1..e7195f41be 100644 --- a/test/rss/rss-testcase.rb +++ b/test/rss/rss-testcase.rb @@ -19,6 +19,7 @@ module RSS LINK_VALUE = "http://xml.com/pub" URL_VALUE = "http://xml.com/universal/images/xml_tiny.gif" NAME_VALUE = "hogehoge" + LANGUAGE_VALUE = "ja" DESCRIPTION_VALUE = " XML.com features a rich mix of information and services for the XML community. @@ -28,6 +29,18 @@ module RSS "http://xml.com/pub/2000/08/09/rdfdb/index.html", ] + CLOUD_DOMAIN = "data.ourfavoritesongs.com" + CLOUD_PORT = "80" + CLOUD_PATH = "/RPC2" + CLOUD_REGISTER_PROCEDURE = "ourFavoriteSongs.rssPleaseNotify" + CLOUD_PROTOCOL = "xml-rpc" + + ENCLOSURE_URL = "http://www.scripting.com/mp3s/weatherReportSuite.mp3" + ENCLOSURE_LENGTH = "12216320" + ENCLOSURE_TYPE = "audio/mpeg" + + CATEGORY_DOMAIN = "http://www.superopendirectory.com/" + def default_test # This class isn't tested end @@ -116,7 +129,7 @@ EOT EOR end - def make_Rss2(content=nil, xmlns=[]) + def make_rss20(content=nil, xmlns=[]) <<-EORSS #{make_xmldecl} #{TITLE_VALUE} #{LINK_VALUE} #{DESCRIPTION_VALUE} + #{LANGUAGE_VALUE} #{RDF_RESOURCE} @@ -142,6 +156,9 @@ EORSS #{RESOURCES.collect do |res| '' + res + '' end.join("\n")} + #{TITLE_VALUE} + #{DESCRIPTION_VALUE} + #{NAME_VALUE} #{RDF_RESOURCE} @@ -150,7 +167,7 @@ EORSS EOC end - def make_item2(content=nil) + def make_item20(content=nil) <<-EOI #{TITLE_VALUE} @@ -160,5 +177,17 @@ EOC EOI end + + def make_cloud20 + <<-EOC + +EOC + end + end end diff --git a/test/rss/test_parser.rb b/test/rss/test_parser.rb index 05ff9748ad..5b15ccb908 100644 --- a/test/rss/test_parser.rb +++ b/test/rss/test_parser.rb @@ -3,6 +3,8 @@ require "rss-testcase" require "rss/1.0" +require "rss/2.0" +require "rss/dublincore" module RSS class TestParser < TestCase @@ -76,14 +78,19 @@ EOR EOR end - assert_not_excepted_tag("image", "RDF") do - Parser.parse(make_RDF(<<-EOR)) + assert_parse(make_RDF(<<-EOR), :nothing_raised) #{make_channel} #{make_item} #{make_image} #{make_textinput} EOR - end + + assert_parse(make_RDF(<<-EOR), :nothing_raised) +#{make_channel} +#{make_item} +#{make_textinput} +#{make_image} +EOR assert_parse(make_RDF(<<-EOR), :nothing_raised) #{make_channel} @@ -279,23 +286,6 @@ EOR EOR - rss = make_RDF(<<-EOR) -#{make_channel} - - hoge - http://example.com/ - http://example.com/hoge.png - -EOR - - assert_missing_tag("url", "image") do - Parser.parse(rss) - end - - assert_missing_tag("item", "RDF") do - Parser.parse(rss, false).validate - end - assert_parse(make_RDF(<<-EOR), :missing_tag, "item", "RDF") #{make_channel} @@ -305,6 +295,23 @@ EOR EOR + rss = make_RDF(<<-EOR) +#{make_channel} + + http://example.com/ + http://example.com/hoge.png + hoge + +EOR + + assert_missing_tag("item", "RDF") do + Parser.parse(rss) + end + + assert_missing_tag("item", "RDF") do + Parser.parse(rss, false).validate + end + end def test_item @@ -439,6 +446,142 @@ EOR end + def test_rss20 + + assert_parse(make_rss20(<<-EOR), :missing_tag, "channel", "rss") +EOR + + assert_parse(make_rss20(<<-EOR), :nothing_raised) +#{make_channel20("")} +EOR + + end + + def test_cloud20 + + attrs = [ + ["domain", CLOUD_DOMAIN], + ["port", CLOUD_PORT], + ["path", CLOUD_PATH], + ["registerProcedure", CLOUD_REGISTER_PROCEDURE], + ["protocol", CLOUD_PROTOCOL], + ] + + (attrs.size + 1).times do |i| + missing_attr = attrs[i] + if missing_attr + meth = :missing_attribute + args = ["cloud", missing_attr[0]] + else + meth = :nothing_raised + args = [] + end + + cloud_attrs = [] + attrs.each_with_index do |attr, j| + unless i == j + cloud_attrs << %Q[#{attr[0]}="#{attr[1]}"] + end + end + + assert_parse(make_rss20(<<-EOR), meth, *args) +#{make_channel20(%Q[])} +EOR + + end + + end + + def test_source20 + + assert_parse(make_rss20(<<-EOR), :missing_attribute, "source", "url") +#{make_channel20(make_item20(%Q[Example]))} +EOR + + assert_parse(make_rss20(<<-EOR), :nothing_raised) +#{make_channel20(make_item20(%Q[]))} +EOR + + assert_parse(make_rss20(<<-EOR), :nothing_raised) +#{make_channel20(make_item20(%Q[Example]))} +EOR + end + + def test_enclosure20 + + attrs = [ + ["url", ENCLOSURE_URL], + ["length", ENCLOSURE_LENGTH], + ["type", ENCLOSURE_TYPE], + ] + + (attrs.size + 1).times do |i| + missing_attr = attrs[i] + if missing_attr + meth = :missing_attribute + args = ["enclosure", missing_attr[0]] + else + meth = :nothing_raised + args = [] + end + + enclosure_attrs = [] + attrs.each_with_index do |attr, j| + unless i == j + enclosure_attrs << %Q[#{attr[0]}="#{attr[1]}"] + end + end + + assert_parse(make_rss20(<<-EOR), meth, *args) +#{make_channel20(%Q[ +#{make_item20(%Q[ + + ])} + ])} +EOR + + end + + end + + def test_category20 + + attrs = [ + ["domain", CATEGORY_DOMAIN], + ] + + (attrs.size + 1).times do |i| + missing_attr = attrs[i] + if missing_attr + meth = :missing_attribute + args = ["category", missing_attr[0]] + else + meth = :nothing_raised + args = [] + end + + category_attrs = [] + attrs.each_with_index do |attr, j| + unless i == j + category_attrs << %Q[#{attr[0]}="#{attr[1]}"] + end + end + + ["", "Example Text"].each do |text| + assert_parse(make_rss20(<<-EOR), meth, *args) +#{make_channel20(%Q[ +#{make_item20(%Q[ +#{text} + ])} + ])} +EOR + end + end + + end + def test_ignore rss = make_RDF(<<-EOR) diff --git a/test/rss/test_trackback.rb b/test/rss/test_trackback.rb index 141ef50779..ab77cf3fb8 100644 --- a/test/rss/test_trackback.rb +++ b/test/rss/test_trackback.rb @@ -40,13 +40,13 @@ EOR @rss = Parser.parse(@rss_source) - @rss2_source = make_Rss2(nil, {@prefix => @uri}) do - make_channel2(nil) do - make_item2(@content_nodes2) + @rss20_source = make_rss20(nil, {@prefix => @uri}) do + make_channel20(nil) do + make_item20(@content_nodes2) end end - @rss2 = Parser.parse(@rss2_source, false) + @rss20 = Parser.parse(@rss20_source, false) end def test_parser @@ -92,18 +92,18 @@ EOR accessor = "#{RSS::TRACKBACK_PREFIX}_#{name}" target_accessor = "resource" target = @rss.send(parent).send(accessor) - target2 = @rss2.channel.send(parent, -1) + target20 = @rss20.channel.send(parent, -1) assert_equal(value, target.send(target_accessor)) - assert_equal(value, target2.send(accessor)) + assert_equal(value, target20.send(accessor)) target.send("#{target_accessor}=", new_value[name].to_s) if name == :about # abount is zero or more - target2.send("#{accessor}=", 0, new_value[name].to_s) + target20.send("#{accessor}=", 0, new_value[name].to_s) else - target2.send("#{accessor}=", new_value[name].to_s) + target20.send("#{accessor}=", new_value[name].to_s) end assert_equal(new_value[name], target.send(target_accessor)) - assert_equal(new_value[name], target2.send(accessor)) + assert_equal(new_value[name], target20.send(accessor)) end end