atom-tools 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +3 -3
- data/README +4 -44
- data/Rakefile +9 -2
- data/bin/atom-cp +159 -0
- data/bin/atom-grep +78 -0
- data/bin/atom-post +72 -0
- data/bin/atom-purge +82 -0
- data/lib/atom/cache.rb +178 -0
- data/lib/atom/collection.rb +77 -17
- data/lib/atom/element.rb +520 -166
- data/lib/atom/entry.rb +82 -142
- data/lib/atom/feed.rb +48 -66
- data/lib/atom/http.rb +115 -35
- data/lib/atom/service.rb +56 -113
- data/lib/atom/text.rb +79 -63
- data/lib/atom/tools.rb +163 -0
- data/test/conformance/order.rb +11 -10
- data/test/conformance/title.rb +9 -9
- data/test/test_constructs.rb +23 -10
- data/test/test_feed.rb +0 -44
- data/test/test_general.rb +0 -40
- data/test/test_http.rb +18 -0
- data/test/test_protocol.rb +60 -22
- data/test/test_xml.rb +73 -41
- metadata +47 -37
- data/bin/atom-client.rb +0 -275
- data/lib/atom/xml.rb +0 -213
- data/lib/atom/yaml.rb +0 -116
data/lib/atom/text.rb
CHANGED
@@ -18,19 +18,57 @@ module Atom
|
|
18
18
|
#
|
19
19
|
# This content of this element can be retrieved in different formats, see #html and #xml
|
20
20
|
class Text < Atom::Element
|
21
|
-
|
21
|
+
atom_attrb :type
|
22
22
|
|
23
|
-
|
24
|
-
@content = value
|
25
|
-
@content ||= "" # in case of nil
|
26
|
-
self["type"] = "text"
|
23
|
+
include AttrEl
|
27
24
|
|
28
|
-
|
25
|
+
on_parse_root do |e,x|
|
26
|
+
type = e.type
|
27
|
+
|
28
|
+
if x.is_a? REXML::Element
|
29
|
+
if type == 'xhtml'
|
30
|
+
x = x.elements['div']
|
31
|
+
raise Atom::ParseError, 'xhtml content needs div wrapper' unless x
|
32
|
+
|
33
|
+
c = x.dup
|
34
|
+
else
|
35
|
+
c = x[0] ? x[0].value : nil
|
36
|
+
end
|
37
|
+
else
|
38
|
+
c = x.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
e.instance_variable_set("@content", c)
|
42
|
+
end
|
43
|
+
|
44
|
+
on_build do |e,x|
|
45
|
+
c = e.instance_variable_get('@content')
|
46
|
+
|
47
|
+
if c.respond_to? :parent
|
48
|
+
x << c.dup
|
49
|
+
elsif c
|
50
|
+
x.text = c.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize value = nil
|
55
|
+
super()
|
56
|
+
|
57
|
+
@content = if value.respond_to? :to_xml
|
58
|
+
value.to_xml[0]
|
59
|
+
elsif value
|
60
|
+
value
|
61
|
+
else
|
62
|
+
''
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def type
|
67
|
+
@type ? @type : 'text'
|
29
68
|
end
|
30
69
|
|
31
|
-
# convenient, but not overly useful. see #html instead.
|
32
70
|
def to_s
|
33
|
-
if
|
71
|
+
if type == 'xhtml' and @content and @content.name == 'div'
|
34
72
|
@content.children.to_s
|
35
73
|
else
|
36
74
|
@content.to_s
|
@@ -56,10 +94,12 @@ module Atom
|
|
56
94
|
# If self["type"] is "html" and Hpricot is installed, it will
|
57
95
|
# be converted to XHTML first.
|
58
96
|
def xml
|
97
|
+
xml = REXML::Element.new 'div'
|
98
|
+
|
59
99
|
if self["type"] == "xhtml"
|
60
|
-
@content.children
|
100
|
+
@content.children.each { |child| xml << child }
|
61
101
|
elsif self["type"] == "text"
|
62
|
-
|
102
|
+
xml.text = self.to_s
|
63
103
|
elsif self["type"] == "html"
|
64
104
|
begin
|
65
105
|
require "hpricot"
|
@@ -68,54 +108,32 @@ module Atom
|
|
68
108
|
end
|
69
109
|
|
70
110
|
fixed = Hpricot(self.to_s, :xhtml_strict => true)
|
71
|
-
REXML::Document.new("<div>#{fixed}</div>").root
|
111
|
+
xml = REXML::Document.new("<div>#{fixed}</div>").root
|
72
112
|
else
|
73
113
|
# XXX check that @type is an XML mimetype and parse it
|
74
114
|
raise "I haven't implemented this yet"
|
75
115
|
end
|
116
|
+
|
117
|
+
xml
|
76
118
|
end
|
77
119
|
|
78
120
|
def inspect # :nodoc:
|
79
121
|
"'#{to_s}'##{self['type']}"
|
80
122
|
end
|
81
123
|
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
raise "atomTextConstruct type '#{value}' is meaningless"
|
86
|
-
end
|
87
|
-
|
88
|
-
if value == "xhtml"
|
89
|
-
begin
|
90
|
-
parse_xhtml_content
|
91
|
-
rescue REXML::ParseException
|
92
|
-
raise "#{@content.inspect} can't be parsed as XML"
|
93
|
-
end
|
94
|
-
end
|
124
|
+
def type= value
|
125
|
+
unless valid_type? value
|
126
|
+
raise Atom::ParseError, "atomTextConstruct type '#{value}' is meaningless"
|
95
127
|
end
|
96
128
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
e.attributes.delete "type"
|
105
|
-
end
|
106
|
-
|
107
|
-
# this should be done via inheritance
|
108
|
-
c = convert_contents e
|
109
|
-
|
110
|
-
if c.is_a? String
|
111
|
-
e.text = c
|
112
|
-
elsif c.is_a? REXML::Element
|
113
|
-
e << c.dup
|
114
|
-
else
|
115
|
-
raise RuntimeError, "atom:#{local_name} can't contain type #{@content.class}"
|
129
|
+
@type = value
|
130
|
+
if @type == "xhtml"
|
131
|
+
begin
|
132
|
+
parse_xhtml_content
|
133
|
+
rescue REXML::ParseException
|
134
|
+
raise Atom::ParseError, "#{@content.inspect} can't be parsed as XML"
|
135
|
+
end
|
116
136
|
end
|
117
|
-
|
118
|
-
e
|
119
137
|
end
|
120
138
|
|
121
139
|
private
|
@@ -123,10 +141,8 @@ module Atom
|
|
123
141
|
def convert_contents e
|
124
142
|
if self["type"] == "xhtml"
|
125
143
|
@content
|
126
|
-
elsif self["type"] == "text" or self["type"].nil?
|
127
|
-
|
128
|
-
elsif self["type"] == "html"
|
129
|
-
@content.to_s.gsub(/&/, "&")
|
144
|
+
elsif self["type"] == "text" or self["type"].nil? or self["type"] == "html"
|
145
|
+
@content.to_s
|
130
146
|
end
|
131
147
|
end
|
132
148
|
|
@@ -164,23 +180,18 @@ module Atom
|
|
164
180
|
# * the "type" attribute can be an arbitrary media type
|
165
181
|
# * there is a "src" attribute which is an IRI that points to the content of the entry (in which case the content element will be empty)
|
166
182
|
class Content < Atom::Text
|
167
|
-
|
183
|
+
is_atom_element :content
|
168
184
|
|
169
|
-
|
170
|
-
if self["src"]
|
171
|
-
""
|
172
|
-
else
|
173
|
-
super
|
174
|
-
end
|
175
|
-
end
|
185
|
+
atom_attrb :src
|
176
186
|
|
177
|
-
def
|
178
|
-
|
179
|
-
element_super = Element.instance_method(:to_element)
|
180
|
-
return element_super.bind(self).call
|
181
|
-
end
|
187
|
+
def src= v
|
188
|
+
@content = nil
|
182
189
|
|
183
|
-
|
190
|
+
if self.base
|
191
|
+
@src = (self.base.to_uri + v).to_s
|
192
|
+
else
|
193
|
+
@src = v
|
194
|
+
end
|
184
195
|
end
|
185
196
|
|
186
197
|
private
|
@@ -202,4 +213,9 @@ module Atom
|
|
202
213
|
s
|
203
214
|
end
|
204
215
|
end
|
216
|
+
|
217
|
+
class Title < Atom::Text; is_atom_element :title; end
|
218
|
+
class Subtitle < Atom::Text; is_atom_element :subtitle; end
|
219
|
+
class Summary < Atom::Text; is_atom_element :summary; end
|
220
|
+
class Rights < Atom::Text; is_atom_element :rights; end
|
205
221
|
end
|
data/lib/atom/tools.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'atom/collection'
|
2
|
+
|
3
|
+
# methods to make writing commandline Atom tools more convenient
|
4
|
+
|
5
|
+
module Atom::Tools
|
6
|
+
# fetch and parse a Feed URL, returning the entries found
|
7
|
+
def http_to_entries url, complete_feed = false, http = Atom::HTTP.new
|
8
|
+
feed = Atom::Feed.new url, http
|
9
|
+
|
10
|
+
if complete_feed
|
11
|
+
feed.get_everything!
|
12
|
+
else
|
13
|
+
feed.update!
|
14
|
+
end
|
15
|
+
|
16
|
+
feed.entries
|
17
|
+
end
|
18
|
+
|
19
|
+
# parse a directory of entries
|
20
|
+
def dir_to_entries path
|
21
|
+
raise ArgumentError, "#{path} is not a directory" unless File.directory? path
|
22
|
+
|
23
|
+
Dir[path+'/*.atom'].map do |e|
|
24
|
+
Atom::Entry.parse(File.read(e))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# parse a Feed on stdin
|
29
|
+
def stdin_to_entries
|
30
|
+
Atom::Feed.parse($stdin).entries
|
31
|
+
end
|
32
|
+
|
33
|
+
# POSTs an Array of Atom::Entrys to an Atom Collection
|
34
|
+
def entries_to_http entries, url, http = Atom::HTTP.new
|
35
|
+
coll = Atom::Collection.new url, http
|
36
|
+
|
37
|
+
entries.each { |entry| coll.post! entry }
|
38
|
+
end
|
39
|
+
|
40
|
+
# saves an Array of Atom::Entrys to a directory
|
41
|
+
def entries_to_dir entries, path
|
42
|
+
if File.exists? path
|
43
|
+
raise "directory #{path} already exists"
|
44
|
+
else
|
45
|
+
Dir.mkdir path
|
46
|
+
end
|
47
|
+
|
48
|
+
entries.each do |entry|
|
49
|
+
e = entry.to_s
|
50
|
+
|
51
|
+
new_filename = path + '/0x' + MD5.new(e).hexdigest[0,8] + '.atom'
|
52
|
+
|
53
|
+
File.open(new_filename, 'w') { |f| f.write e }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# dumps an Array of Atom::Entrys into a Feed on stdout
|
58
|
+
def entries_to_stdout entries
|
59
|
+
feed = Atom::Feed.new
|
60
|
+
|
61
|
+
entries.each do |entry|
|
62
|
+
puts entry.inspect
|
63
|
+
feed.entries << entry
|
64
|
+
end
|
65
|
+
|
66
|
+
puts feed.to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
# turns a collection of Atom Entries into an Array of Atom::Entrys
|
70
|
+
#
|
71
|
+
# source: a URL, a directory or "-" for an Atom Feed on stdin
|
72
|
+
# options:
|
73
|
+
# :complete - whether to fetch the complete logical feed
|
74
|
+
# :user - username to use for HTTP requests (if required)
|
75
|
+
# :pass - password to use for HTTP requests (if required)
|
76
|
+
def parse_input source, options
|
77
|
+
entries = if source.match /^http/
|
78
|
+
http = Atom::HTTP.new
|
79
|
+
|
80
|
+
setup_http http, options
|
81
|
+
|
82
|
+
http_to_entries source, options[:complete], http
|
83
|
+
elsif source == '-'
|
84
|
+
stdin_to_entries
|
85
|
+
else
|
86
|
+
dir_to_entries source
|
87
|
+
end
|
88
|
+
|
89
|
+
if options[:verbose]
|
90
|
+
entries.each do |entry|
|
91
|
+
puts "got #{entry.title}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
entries
|
96
|
+
end
|
97
|
+
|
98
|
+
# turns an Array of Atom::Entrys into a collection of Atom Entries
|
99
|
+
#
|
100
|
+
# entries: an Array of Atom::Entrys pairs
|
101
|
+
# dest: a URL, a directory or "-" for an Atom Feed on stdout
|
102
|
+
# options:
|
103
|
+
# :user - username to use for HTTP requests (if required)
|
104
|
+
# :pass - password to use for HTTP requests (if required)
|
105
|
+
def write_output entries, dest, options
|
106
|
+
if dest.match /^http/
|
107
|
+
http = Atom::HTTP.new
|
108
|
+
|
109
|
+
setup_http http, options
|
110
|
+
|
111
|
+
entries_to_http entries, dest, http
|
112
|
+
elsif dest == '-'
|
113
|
+
entries_to_stdout entries
|
114
|
+
else
|
115
|
+
entries_to_dir entries, dest
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# set up some common OptionParser settings
|
120
|
+
def atom_options opts, options
|
121
|
+
opts.on('-u', '--user NAME', 'username for HTTP auth') { |u| options[:user] = u }
|
122
|
+
|
123
|
+
opts.on_tail('-h', '--help', 'show this usage statement') { |h| puts opts; exit }
|
124
|
+
|
125
|
+
opts.on_tail('-p', '--password [PASSWORD]', 'password for HTTP auth') do |p|
|
126
|
+
options[:pass] = p
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# obtain a password from the TTY, hiding the user's input
|
132
|
+
# this will fail if you don't have the program 'stty'
|
133
|
+
def obtain_password
|
134
|
+
i = o = File.open('/dev/tty', 'w+')
|
135
|
+
|
136
|
+
o.print 'Password: '
|
137
|
+
|
138
|
+
# store original settings
|
139
|
+
state = `stty -F /dev/tty -g`
|
140
|
+
|
141
|
+
# don't echo input
|
142
|
+
system "stty -F /dev/tty -echo"
|
143
|
+
|
144
|
+
p = i.gets.chomp
|
145
|
+
|
146
|
+
# restore original settings
|
147
|
+
system "stty -F /dev/tty #{state}"
|
148
|
+
|
149
|
+
p
|
150
|
+
end
|
151
|
+
|
152
|
+
def setup_http http, options
|
153
|
+
if options[:user]
|
154
|
+
http.user = options[:user]
|
155
|
+
|
156
|
+
unless options[:pass]
|
157
|
+
options[:pass] = obtain_password
|
158
|
+
end
|
159
|
+
|
160
|
+
http.pass = options[:pass]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/test/conformance/order.rb
CHANGED
@@ -9,23 +9,23 @@ FEED.update!
|
|
9
9
|
class TestOrderConformance < Test::Unit::TestCase
|
10
10
|
def test_0
|
11
11
|
entry = FEED.entries[0]
|
12
|
-
|
12
|
+
|
13
13
|
assert_equal "tag:example.org,2006:atom/conformance/element_order/1", entry.id
|
14
14
|
assert_equal "Simple order, nothing fancy", entry.title.to_s
|
15
15
|
assert_equal "Simple ordering, nothing fancy", entry.summary.to_s
|
16
16
|
assert_equal Time.parse("2006-01-26T09:20:01Z"), entry.updated
|
17
|
-
|
17
|
+
|
18
18
|
assert_alternate_href(entry, "http://www.snellspace.com/public/alternate")
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_1
|
22
22
|
entry = FEED.entries[1]
|
23
|
-
|
23
|
+
|
24
24
|
assert_equal "tag:example.org,2006:atom/conformance/element_order/2", entry.id
|
25
25
|
assert_equal "Same as the first, only mixed up a bit", entry.title.to_s
|
26
26
|
assert_equal "Same as the first, only mixed up a bit", entry.summary.to_s
|
27
27
|
assert_equal Time.parse("2006-01-26T09:20:02Z"), entry.updated
|
28
|
-
|
28
|
+
|
29
29
|
assert_alternate_href(entry, "http://www.snellspace.com/public/alternate")
|
30
30
|
end
|
31
31
|
|
@@ -34,8 +34,9 @@ class TestOrderConformance < Test::Unit::TestCase
|
|
34
34
|
entry = FEED.entries[2]
|
35
35
|
|
36
36
|
# both links should be available, but it's up to you to choose which one to use
|
37
|
+
|
37
38
|
assert_link_href(entry, "http://www.snellspace.com/public/alternate") { |l| l["rel"] == "alternate" and l["type"] == nil }
|
38
|
-
|
39
|
+
|
39
40
|
assert_link_href(entry, "http://www.snellspace.com/public/alternate2") { |l| l["rel"] == "alternate" and l["type"] == "text/plain" }
|
40
41
|
end
|
41
42
|
|
@@ -65,7 +66,7 @@ class TestOrderConformance < Test::Unit::TestCase
|
|
65
66
|
# ^-- quoted summary is a typo, source is last
|
66
67
|
def test_5
|
67
68
|
entry = FEED.entries[5]
|
68
|
-
|
69
|
+
|
69
70
|
assert_equal "tag:example.org,2006:atom/conformance/element_order/6", entry.id
|
70
71
|
assert_equal "Entry with a source last", entry.title.to_s
|
71
72
|
assert_equal Time.parse("2006-01-26T09:20:06Z"), entry.updated
|
@@ -91,18 +92,18 @@ class TestOrderConformance < Test::Unit::TestCase
|
|
91
92
|
assert_equal "tag:example.org,2006:atom/conformance/element_order/8", entry.id
|
92
93
|
assert_equal "Atom elements in an extension element", entry.title.to_s
|
93
94
|
assert_equal Time.parse("2006-01-26T09:20:08Z"), entry.updated
|
94
|
-
|
95
|
+
|
95
96
|
assert_alternate_href(entry, "http://www.snellspace.com/public/alternate")
|
96
97
|
end
|
97
|
-
|
98
|
+
|
98
99
|
# Atom elements in an extension element
|
99
100
|
def test_8
|
100
101
|
entry = FEED.entries[8]
|
101
|
-
|
102
|
+
|
102
103
|
assert_equal "tag:example.org,2006:atom/conformance/element_order/9", entry.id
|
103
104
|
assert_equal "Atom elements in an extension element", entry.title.to_s
|
104
105
|
assert_equal Time.parse("2006-01-26T09:20:09Z"), entry.updated
|
105
|
-
|
106
|
+
|
106
107
|
assert_alternate_href(entry, "http://www.snellspace.com/public/alternate")
|
107
108
|
end
|
108
109
|
|
data/test/conformance/title.rb
CHANGED
@@ -25,18 +25,18 @@ class TestTitleConformance < Test::Unit::TestCase
|
|
25
25
|
feed.update!
|
26
26
|
|
27
27
|
entry = feed.entries.first
|
28
|
-
assert_equal "html", entry.title["type"]
|
28
|
+
assert_equal "html", entry.title["type"]
|
29
29
|
assert_equal "<title>", entry.title.html
|
30
30
|
end
|
31
31
|
|
32
32
|
def test_html_entity
|
33
33
|
url = "http://atomtests.philringnalda.com/tests/item/title/html-entity.atom"
|
34
|
-
|
34
|
+
|
35
35
|
feed = Atom::Feed.new(url)
|
36
36
|
feed.update!
|
37
37
|
|
38
38
|
entry = feed.entries.first
|
39
|
-
assert_equal "html", entry.title["type"]
|
39
|
+
assert_equal "html", entry.title["type"]
|
40
40
|
assert_equal "<title>", entry.title.html
|
41
41
|
end
|
42
42
|
|
@@ -47,7 +47,7 @@ class TestTitleConformance < Test::Unit::TestCase
|
|
47
47
|
feed.update!
|
48
48
|
|
49
49
|
entry = feed.entries.first
|
50
|
-
assert_equal "html", entry.title["type"]
|
50
|
+
assert_equal "html", entry.title["type"]
|
51
51
|
assert_equal "<title>", entry.title.html
|
52
52
|
end
|
53
53
|
|
@@ -78,7 +78,7 @@ class TestTitleConformance < Test::Unit::TestCase
|
|
78
78
|
|
79
79
|
feed = Atom::Feed.new(url)
|
80
80
|
feed.update!
|
81
|
-
|
81
|
+
|
82
82
|
entry = feed.entries.first
|
83
83
|
assert_equal "text", entry.title["type"]
|
84
84
|
assert_equal "<title>", entry.title.html
|
@@ -86,10 +86,10 @@ class TestTitleConformance < Test::Unit::TestCase
|
|
86
86
|
|
87
87
|
def test_xhtml_entity
|
88
88
|
url = "http://atomtests.philringnalda.com/tests/item/title/xhtml-entity.atom"
|
89
|
-
|
89
|
+
|
90
90
|
feed = Atom::Feed.new(url)
|
91
91
|
feed.update!
|
92
|
-
|
92
|
+
|
93
93
|
entry = feed.entries.first
|
94
94
|
assert_equal "xhtml", entry.title["type"]
|
95
95
|
assert_equal "<title>", entry.title.html
|
@@ -97,10 +97,10 @@ class TestTitleConformance < Test::Unit::TestCase
|
|
97
97
|
|
98
98
|
def test_xhtml_ncr
|
99
99
|
url = "http://atomtests.philringnalda.com/tests/item/title/xhtml-ncr.atom"
|
100
|
-
|
100
|
+
|
101
101
|
feed = Atom::Feed.new(url)
|
102
102
|
feed.update!
|
103
|
-
|
103
|
+
|
104
104
|
entry = feed.entries.first
|
105
105
|
assert_equal "xhtml", entry.title["type"]
|
106
106
|
assert_equal "<title>", entry.title.html
|