ape 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|