ape 1.5.1 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +1 -1
- data/Rakefile +14 -6
- data/bin/ape_server +1 -1
- data/lib/ape.rb +49 -36
- data/lib/ape/atomURI.rb +2 -2
- data/lib/ape/coll_element.rb +67 -0
- data/lib/ape/collection.rb +30 -33
- data/lib/ape/entry.rb +22 -10
- data/lib/ape/feed.rb +1 -1
- data/lib/ape/invoker.rb +17 -0
- data/lib/ape/invokers/getter.rb +18 -2
- data/lib/ape/invokers/poster.rb +6 -7
- data/lib/ape/names.rb +2 -1
- data/lib/ape/reporter.rb +1 -1
- data/lib/ape/samples.rb +2 -2
- data/lib/ape/server.rb +1 -1
- data/lib/ape/service.rb +20 -3
- data/lib/ape/util.rb +11 -7
- data/lib/ape/validator.rb +1 -1
- data/lib/ape/validators/entry_posts_validator.rb +2 -3
- data/lib/ape/validators/media_linkage_validator.rb +2 -2
- data/lib/ape/validators/media_posts_validator.rb +1 -2
- data/lib/ape/validators/sanitization_validator.rb +1 -1
- data/lib/ape/validators/service_document_validator.rb +14 -24
- data/lib/ape/version.rb +4 -4
- data/test/test_helper.rb +24 -1
- data/test/unit/ape_test.rb +8 -24
- data/test/unit/coll_element_test.rb +56 -0
- data/test/unit/collection_test.rb +43 -0
- data/test/unit/invoker_test.rb +6 -0
- data/test/unit/samples_test.rb +1 -1
- data/test/unit/service_test.rb +47 -0
- data/web/index.html +1 -1
- metadata +59 -51
data/README
CHANGED
@@ -29,7 +29,7 @@ If you've downloaded APE's source code you can execute it from the command line
|
|
29
29
|
$ rake ape:go:text['service document uri', 'username', 'password']
|
30
30
|
$ rake ape:go:atom['service document uri', 'username', 'password']
|
31
31
|
|
32
|
-
The username and password
|
32
|
+
The username and password parameters are not mandatory, and the format response depends on the executed task.
|
33
33
|
|
34
34
|
== The Source
|
35
35
|
|
data/Rakefile
CHANGED
@@ -2,6 +2,7 @@ require 'rake'
|
|
2
2
|
require 'rake/clean'
|
3
3
|
require 'rake/gempackagetask'
|
4
4
|
require 'rake/testtask'
|
5
|
+
require 'rake/rdoctask'
|
5
6
|
|
6
7
|
$:.unshift File.dirname(__FILE__) + '/lib'
|
7
8
|
require 'ape'
|
@@ -11,9 +12,9 @@ def spec
|
|
11
12
|
s.platform = Gem::Platform::RUBY
|
12
13
|
s.name = 'ape'
|
13
14
|
s.version = Ape::VERSION::STRING
|
14
|
-
s.
|
15
|
-
s.email = 'tim.bray@sun.com'
|
16
|
-
s.homepage = 'ape.rubyforge.org'
|
15
|
+
s.authors = ['Tim Bray', 'David Calavera']
|
16
|
+
s.email = ['tim.bray@sun.com', 'calavera@apache.org']
|
17
|
+
s.homepage = 'http://ape.rubyforge.org'
|
17
18
|
s.summary = 'The Atom Protocol Exerciser'
|
18
19
|
|
19
20
|
s.files = FileList['lib/**/*', 'samples/*', 'test/**/*', 'web/*',
|
@@ -21,7 +22,7 @@ def spec
|
|
21
22
|
s.bindir = 'bin'
|
22
23
|
s.executable = 'ape_server'
|
23
24
|
|
24
|
-
s.has_rdoc =
|
25
|
+
s.has_rdoc = true
|
25
26
|
s.extra_rdoc_files = ['README', 'LICENSE']
|
26
27
|
|
27
28
|
s.rubyforge_project = 'ape'
|
@@ -45,7 +46,7 @@ task :setup do
|
|
45
46
|
installed = Gem::SourceIndex.from_installed_gems
|
46
47
|
dependencies = spec.dependencies
|
47
48
|
dependencies.select { |dep|
|
48
|
-
installed.
|
49
|
+
installed.find_name(dep.name, dep.version_requirements).empty? }.each do |dep|
|
49
50
|
puts "Installing #{dep} ..."
|
50
51
|
install_gem dep.name, "-v '#{dep.version_requirements.to_s}'"
|
51
52
|
end
|
@@ -63,4 +64,11 @@ task :test => [:test_units]
|
|
63
64
|
Rake::TestTask.new("test_units") do |t|
|
64
65
|
t.test_files = FileList['test/unit/*test.rb']
|
65
66
|
t.verbose = false
|
66
|
-
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Rdoc ---------------------------------------------------------------
|
70
|
+
|
71
|
+
Rake::RDocTask.new do |t|
|
72
|
+
t.main = 'README'
|
73
|
+
t.rdoc_files.include('README', 'LICENSE', 'lib/**/*.rb')
|
74
|
+
end
|
data/bin/ape_server
CHANGED
@@ -20,7 +20,7 @@ parser = OptionParser.new do |opts|
|
|
20
20
|
opts.separator ''
|
21
21
|
opts.on('-a', '--address ADDRESS', 'Address to bind to', "default: #{OPTIONS[:host]}") { |v| OPTIONS[:host] = v }
|
22
22
|
opts.on('-p', '--port PORT', 'Port to bind to', "default: #{OPTIONS[:port]}") { |v| OPTIONS[:port] = v }
|
23
|
-
opts.on('-d', '--directory DIRECTORY', 'ape home directory', "default: #{
|
23
|
+
opts.on('-d', '--directory DIRECTORY', 'ape home directory', "default: #{::Ape.home}") { |v| OPTIONS[:home] = v }
|
24
24
|
opts.on('-h', '--help', 'Displays this help') { puts opts; exit }
|
25
25
|
opts.parse!(ARGV)
|
26
26
|
end
|
data/lib/ape.rb
CHANGED
@@ -4,7 +4,7 @@ $:.unshift File.dirname(__FILE__)
|
|
4
4
|
module Ape
|
5
5
|
require 'rubygems'
|
6
6
|
|
7
|
-
Dir[File.dirname(__FILE__) + '/ape/*.rb'].each { |l| require l }
|
7
|
+
Dir[File.dirname(__FILE__) + '/ape/*.rb'].sort.each { |l| require l }
|
8
8
|
|
9
9
|
@CONF = {}
|
10
10
|
|
@@ -12,32 +12,28 @@ module Ape
|
|
12
12
|
@CONF
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@@home = nil
|
19
|
-
def self.home=(home)
|
20
|
-
@@home = home
|
21
|
-
end
|
15
|
+
def Ape.home=(home)
|
16
|
+
@CONF[:HOME] = home
|
17
|
+
end
|
22
18
|
|
23
|
-
|
24
|
-
|
25
|
-
|
19
|
+
def Ape.home
|
20
|
+
@CONF[:HOME] || ENV["APE_HOME"] || File.join(home_directory,".ape")
|
21
|
+
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
def Ape.home_directory
|
24
|
+
ENV["HOME"] || (ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") || "/"
|
25
|
+
end
|
26
|
+
|
27
|
+
class Ape
|
32
28
|
# Creates an Ape instance with options given in the +args+ Hash.
|
33
29
|
#
|
34
30
|
# ==== Options
|
35
|
-
# * :output - one of 'text' or 'html'. #report will output in this format. Defaults to 'html'.
|
31
|
+
# * :output - one of 'text' or 'html' or 'atom'. #report will output in this format. Defaults to 'html'.
|
36
32
|
# * :debug - enables debug information at each step in the output
|
37
33
|
def initialize(args = {})
|
38
34
|
output = args[:output] || 'html'
|
39
35
|
@reporter = Reporter.instance(output, args)
|
40
|
-
load File.join(Ape.home, 'aperc') if File.exist?(File.join(Ape.home, 'aperc'))
|
36
|
+
load File.join(::Ape.home, 'aperc') if File.exist?(File.join(::Ape.home, 'aperc'))
|
41
37
|
end
|
42
38
|
|
43
39
|
# Checks the AtomPub server at +uri+ for sanity.
|
@@ -46,44 +42,54 @@ module Ape
|
|
46
42
|
# * uri - the URI of the AtomPub server. Required.
|
47
43
|
# * username - an optional username for authentication
|
48
44
|
# * password - if a username is provided, a password is required. See Ape::Authent for more information.
|
45
|
+
# * service_doc - an optional service document. It'll be used instead of getting it from the uri.
|
49
46
|
# * requested_e_coll - a preferred entry collection to check
|
50
47
|
# * requested_m_coll - a preferred media collection to check
|
51
|
-
def check(uri, username=nil, password=nil,
|
48
|
+
def check(uri, username=nil, password=nil, service_doc = nil,
|
52
49
|
requested_e_coll = nil, requested_m_coll = nil)
|
53
50
|
|
54
51
|
@authent = Authent.new(username, password)
|
55
|
-
reporter.header = uri
|
52
|
+
@reporter.header = uri
|
56
53
|
::Ape.conf[:REQUESTED_ENTRY_COLLECTION] = requested_e_coll if requested_e_coll
|
57
54
|
::Ape.conf[:REQUESTED_MEDIA_COLLECTION] = requested_m_coll if requested_m_coll
|
58
55
|
begin
|
59
|
-
might_fail(uri)
|
56
|
+
might_fail(uri, service_doc)
|
60
57
|
rescue Exception
|
61
|
-
reporter.error(self, "Ouch! Ape fall down go boom; details: " +
|
58
|
+
@reporter.error(self, "Ouch! Ape fall down go boom; details: " +
|
62
59
|
"#{$!}\n#{$!.class}\n#{$!.backtrace}")
|
63
60
|
end
|
64
61
|
end
|
65
62
|
|
66
|
-
def might_fail(uri)
|
67
|
-
|
68
|
-
|
69
|
-
|
63
|
+
def might_fail(uri, service = nil)
|
64
|
+
unless (service)
|
65
|
+
service_validator = Validator.instance(:service_document, @reporter, @authent)
|
66
|
+
service_validator.validate(:uri => uri)
|
67
|
+
@service = service_validator.service_document
|
68
|
+
else
|
69
|
+
@service = service.instance_of?(String)?
|
70
|
+
REXML::Document.new(service, { :raw => nil }) : service
|
71
|
+
end
|
70
72
|
@entry_collections = service_validator.entry_collections
|
71
73
|
@media_collections = service_validator.media_collections
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
unless true == ::Ape.conf[:ENTRY_VALIDATION_DISABLED]
|
76
|
+
if @entry_collections
|
77
|
+
[:entry_posts, :sorting, :sanitization].each do |option|
|
78
|
+
check_validator(option)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
@reporter.warning(self, "No collection for 'application/atom+xml;type=entry', won't test entry posting.")
|
76
82
|
end
|
77
|
-
else
|
78
|
-
reporter.warning(self, "No collection for 'application/atom+xml;type=entry', won't test entry posting.")
|
79
83
|
end
|
80
84
|
|
81
|
-
|
82
|
-
|
83
|
-
|
85
|
+
unless true == ::Ape.conf[:MEDIA_VALIDATION_DISABLED]
|
86
|
+
if @media_collections
|
87
|
+
[:media_posts, :media_linkage].each do |option|
|
88
|
+
check_validator(option)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
@reporter.warning(self, "No collection for 'image/jpeg', won't test media posting.")
|
84
92
|
end
|
85
|
-
else
|
86
|
-
reporter.warning(self, "No collection for 'image/jpeg', won't test media posting.")
|
87
93
|
end
|
88
94
|
|
89
95
|
#custom validators
|
@@ -92,6 +98,13 @@ module Ape
|
|
92
98
|
break if !validator.validate(opts) && validator.deterministic?
|
93
99
|
end
|
94
100
|
end
|
101
|
+
|
102
|
+
def error_count
|
103
|
+
@reporter.errors.size
|
104
|
+
end
|
105
|
+
def warning_count
|
106
|
+
@reporter.warnings.size
|
107
|
+
end
|
95
108
|
|
96
109
|
def report(output=STDOUT)
|
97
110
|
@reporter.report(output)
|
data/lib/ape/atomURI.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
# Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
|
1
|
+
# Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
|
2
2
|
# Use is subject to license terms - see file "LICENSE"
|
3
3
|
require 'uri'
|
4
4
|
|
5
5
|
module Ape
|
6
6
|
class AtomURI
|
7
|
-
|
7
|
+
|
8
8
|
def initialize base_uri
|
9
9
|
if base_uri.kind_of? URI
|
10
10
|
@base = base_uri
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
# Represents an <app:collection> element as found in an AtomPub Service Doc
|
5
|
+
|
6
|
+
module Ape
|
7
|
+
class CollElement
|
8
|
+
|
9
|
+
@@mime_re = %r{^(.*)/(.*)$}
|
10
|
+
|
11
|
+
attr_reader :title, :accept, :href, :xml
|
12
|
+
|
13
|
+
def CollElement::find_colls(source, doc_uri = nil)
|
14
|
+
els = REXML::XPath.match(source, '//app:collection', Names::XmlNamespaces)
|
15
|
+
els.map { |el| CollElement.new(el, doc_uri) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(el, doc_uri = nil)
|
19
|
+
@xml = el
|
20
|
+
@accept = []
|
21
|
+
@title = REXML::XPath.first(el, './atom:title', Names::XmlNamespaces)
|
22
|
+
|
23
|
+
# sigh, RNC validation *should* take care of this
|
24
|
+
raise(SyntaxError, "Collection is missing required 'atom:title'") unless @title
|
25
|
+
@title = @title.texts.join
|
26
|
+
|
27
|
+
if doc_uri
|
28
|
+
uris = AtomURI.new(doc_uri)
|
29
|
+
@href = uris.absolutize(el.attributes['href'], el)
|
30
|
+
else
|
31
|
+
@href = el.attributes['href']
|
32
|
+
end
|
33
|
+
|
34
|
+
# now we have to go looking for the accept
|
35
|
+
@accept = REXML::XPath.match(@xml, './app:accept/(text)', Names::XmlNamespaces)
|
36
|
+
@accept = [ Names::AtomEntryMediaType ] if @accept.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
# check if the collection accepts a given mime type; watch out for wildcards
|
40
|
+
def accept?(mime_type)
|
41
|
+
if mime_type =~ @@mime_re
|
42
|
+
p1, p2 = $1, $2
|
43
|
+
else
|
44
|
+
return false # WTF?
|
45
|
+
end
|
46
|
+
@accept.each do |pat|
|
47
|
+
pat = pat.to_s # working around REXML ticket 164
|
48
|
+
if pat =~ @@mime_re
|
49
|
+
if ($1 == p1 || $1 == "*") && ($2 == p2 || $2 == "*")
|
50
|
+
return true
|
51
|
+
elsif ((pat == Names::AtomMediaType && mime_type == Names::AtomFeedMediaType) ||
|
52
|
+
(pat == Names::AtomFeedMediaType && mime_type == Names::AtomMediaType))
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
|
60
|
+
# the name is supposed to suggest multiple instances of "categories"
|
61
|
+
def catses
|
62
|
+
REXML::XPath.match(@xml, './app:categories', Names::XmlNamespaces)
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
data/lib/ape/collection.rb
CHANGED
@@ -1,47 +1,44 @@
|
|
1
1
|
# Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
|
2
2
|
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
# Represents an AtomPub collection, which offers feed-reading and resource-CRUD
|
5
|
+
# services
|
3
6
|
require 'rexml/xpath'
|
4
7
|
|
5
8
|
module Ape
|
6
9
|
class Collection
|
7
|
-
attr_reader :title, :accept, :href
|
8
|
-
|
9
|
-
def initialize(input, doc_uri = nil)
|
10
|
-
@input = input
|
11
|
-
@accept = []
|
12
|
-
@title = REXML::XPath.first(input, './atom:title', Names::XmlNamespaces)
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
# The argument has to be an absolute URI
|
12
|
+
#
|
13
|
+
def initialize(uri, authent = nil)
|
14
|
+
@uri = uri
|
15
|
+
@authent = authent
|
16
|
+
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
# Post a new element to this collection; return either an Ape::Entry or
|
19
|
+
# an error-message
|
20
|
+
#
|
21
|
+
# ==== Options
|
22
|
+
# * :data - element to post as a string
|
23
|
+
# * :type - content type. By default 'application/atom+xml;type=entry'
|
24
|
+
# * :slug - slug header
|
25
|
+
#
|
26
|
+
def post(opts = {})
|
27
|
+
return ':data argument not provided' unless opts[:data]
|
28
|
+
|
29
|
+
type = opts[:type] || Names::AtomEntryMediaType
|
30
|
+
@invoker = Poster.new(@uri, @authent)
|
31
|
+
@invoker['Slug'] = opts[:slug] if opts[:slug]
|
32
|
+
|
33
|
+
if @invoker.post(type, opts[:data])
|
34
|
+
@invoker.entry
|
21
35
|
else
|
22
|
-
@
|
36
|
+
@invoker.last_error
|
23
37
|
end
|
24
|
-
|
25
|
-
# now we have to go looking for the accept
|
26
|
-
@accept = []
|
27
|
-
REXML::XPath.each(input, './app:accept', Names::XmlNamespaces) do |a|
|
28
|
-
@accept << a.texts.join
|
29
|
-
end
|
30
|
-
|
31
|
-
@accept = [ Names::AtomEntryMediaType ] if @accept.empty?
|
32
38
|
end
|
33
|
-
|
34
|
-
def
|
35
|
-
@
|
36
|
-
end
|
37
|
-
|
38
|
-
def to_str
|
39
|
-
to_s
|
40
|
-
end
|
41
|
-
|
42
|
-
# the name is supposed to suggest multiple instances of "categories"
|
43
|
-
def catses
|
44
|
-
REXML::XPath.match(@input, './app:categories', Names::XmlNamespaces)
|
39
|
+
|
40
|
+
def crumbs
|
41
|
+
return @invoker.crumbs
|
45
42
|
end
|
46
43
|
end
|
47
44
|
end
|
data/lib/ape/entry.rb
CHANGED
@@ -10,18 +10,26 @@ module Ape
|
|
10
10
|
class Entry
|
11
11
|
# @element is the REXML dom
|
12
12
|
# @base is the base URI if known
|
13
|
+
attr_reader :uri
|
13
14
|
|
14
|
-
def initialize(
|
15
|
-
if
|
16
|
-
@element =
|
15
|
+
def initialize(opts)
|
16
|
+
if opts[:xml]
|
17
|
+
@element = opts[:xml]
|
18
|
+
elsif opts[:text]
|
19
|
+
@element = REXML::Document.new(opts[:text], { :raw => nil }).root
|
17
20
|
else
|
18
|
-
|
21
|
+
raise(ArgumentError, "Neither :xml nor :text provided")
|
19
22
|
end
|
20
|
-
|
21
|
-
|
23
|
+
|
24
|
+
if opts[:uri]
|
25
|
+
@uri = opts[:uri]
|
26
|
+
@base = AtomURI.new(@uri)
|
22
27
|
else
|
23
|
-
@base = nil
|
28
|
+
@base = @uri = nil
|
24
29
|
end
|
30
|
+
|
31
|
+
@authoritative = opts[:authoritative] || false
|
32
|
+
@etag = opts[:etag] || nil
|
25
33
|
end
|
26
34
|
|
27
35
|
def to_s
|
@@ -29,7 +37,7 @@ module Ape
|
|
29
37
|
end
|
30
38
|
|
31
39
|
def content_src
|
32
|
-
content = REXML::XPath.first(@element, './atom:content', Names::XmlNamespaces)
|
40
|
+
content = REXML::XPath.first(@element, './atom:content[@src]', Names::XmlNamespaces)
|
33
41
|
if content
|
34
42
|
cs = content.attributes['src']
|
35
43
|
cs = @base.absolutize(cs, @element) if @base
|
@@ -40,9 +48,8 @@ module Ape
|
|
40
48
|
|
41
49
|
def get_child(field, namespace = nil)
|
42
50
|
if (namespace)
|
43
|
-
thisNS = {}
|
44
51
|
prefix = 'NN'
|
45
|
-
thisNS
|
52
|
+
thisNS = { prefix => namespace }
|
46
53
|
else
|
47
54
|
prefix = 'atom'
|
48
55
|
thisNS = Names::XmlNamespaces
|
@@ -147,5 +154,10 @@ module Ape
|
|
147
154
|
end
|
148
155
|
Nodes.each_node(node.getChildNodes) {|child| dump(child, depth+1)}
|
149
156
|
end
|
157
|
+
|
158
|
+
def to_s
|
159
|
+
@element.to_s
|
160
|
+
end
|
161
|
+
|
150
162
|
end
|
151
163
|
end
|
data/lib/ape/feed.rb
CHANGED
@@ -63,7 +63,7 @@ module Ape
|
|
63
63
|
reporter.info(self, "#{label} has no entries.")
|
64
64
|
end
|
65
65
|
|
66
|
-
entries += page_entries.map { |e| Entry.new(e, next_page)}
|
66
|
+
entries += page_entries.map { |e| Entry.new(:xml => e, :uri => next_page)}
|
67
67
|
|
68
68
|
next_link = REXML::XPath.first(feed, "./atom:link[@rel=\"next\"]", Names::XmlNamespaces)
|
69
69
|
if next_link
|
data/lib/ape/invoker.rb
CHANGED
@@ -8,6 +8,8 @@ module Ape
|
|
8
8
|
|
9
9
|
def initialize(uriString, authent)
|
10
10
|
@last_error = nil
|
11
|
+
|
12
|
+
# XXX really need a way to turn crumbs on/off, mostly off
|
11
13
|
@crumbs = Crumbs.new
|
12
14
|
@uri = AtomURI.check(uriString)
|
13
15
|
if (@uri.class == String)
|
@@ -15,6 +17,21 @@ module Ape
|
|
15
17
|
end
|
16
18
|
@authent = authent
|
17
19
|
@authent_checker = 0
|
20
|
+
@headers = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
#Add a new request header
|
24
|
+
def []=(name, val)
|
25
|
+
set_header(name, val)
|
26
|
+
end
|
27
|
+
|
28
|
+
#get a request header
|
29
|
+
def [](name)
|
30
|
+
@headers[name]
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_header(name, val)
|
34
|
+
@headers[name] = val
|
18
35
|
end
|
19
36
|
|
20
37
|
def header(which)
|