atom-tools 1.0.0 → 2.0.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/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
|