mingle_events 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/Gemfile +2 -4
  2. data/lib/mingle_events.rb +19 -3
  3. data/lib/mingle_events/entry_cache.rb +129 -0
  4. data/lib/mingle_events/feed/author.rb +12 -23
  5. data/lib/mingle_events/feed/category.rb +2 -0
  6. data/lib/mingle_events/feed/changes.rb +19 -50
  7. data/lib/mingle_events/feed/entry.rb +16 -21
  8. data/lib/mingle_events/feed/links.rb +3 -3
  9. data/lib/mingle_events/feed/page.rb +5 -5
  10. data/lib/mingle_events/http.rb +50 -0
  11. data/lib/mingle_events/mingle_basic_auth_access.rb +17 -50
  12. data/lib/mingle_events/mingle_hmac_auth_access.rb +22 -0
  13. data/lib/mingle_events/mingle_oauth_access.rb +10 -30
  14. data/lib/mingle_events/poller.rb +4 -3
  15. data/lib/mingle_events/processors/author_filter.rb +7 -7
  16. data/lib/mingle_events/processors/card_data.rb +15 -15
  17. data/lib/mingle_events/project_custom_properties.rb +15 -15
  18. data/lib/mingle_events/project_event_fetcher.rb +21 -110
  19. data/lib/mingle_events/xml.rb +101 -0
  20. data/lib/mingle_events/zip_directory.rb +98 -0
  21. data/test/mingle_events/entry_cache_test.rb +75 -0
  22. data/test/mingle_events/feed/author_test.rb +2 -2
  23. data/test/mingle_events/feed/changes_test.rb +22 -22
  24. data/test/mingle_events/feed/entry_test.rb +12 -12
  25. data/test/mingle_events/feed/links_test.rb +5 -5
  26. data/test/mingle_events/mingle_basic_auth_access_test.rb +15 -0
  27. data/test/mingle_events/mingle_hmac_auth_access_test.rb +15 -0
  28. data/test/mingle_events/mingle_oauth_access_test.rb +15 -0
  29. data/test/mingle_events/poller_test.rb +1 -1
  30. data/test/mingle_events/processors/author_filter_test.rb +5 -5
  31. data/test/mingle_events/processors/card_data_test.rb +12 -17
  32. data/test/mingle_events/project_custom_properties_test.rb +2 -3
  33. data/test/mingle_events/project_event_fetcher_test.rb +64 -8
  34. data/test/mingle_events/xml_test.rb +80 -0
  35. data/test/mingle_events/zip_directory_test.rb +44 -0
  36. data/test/test_helper.rb +36 -27
  37. metadata +121 -62
@@ -0,0 +1,101 @@
1
+ module MingleEvents
2
+ module Xml
3
+
4
+ class Element
5
+ attr_reader :node, :namespaces
6
+ def initialize(node, namespaces)
7
+ @node = node
8
+ @namespaces = namespaces
9
+ end
10
+
11
+ def nil?
12
+ @node.nil?
13
+ end
14
+
15
+ ["inner_text", "optional_inner_text", "select", "select_all", "attr", "children", "tag_name", "raw_xml", "attributes", "to_hash"].each do |method|
16
+ self.class_eval(%{def #{method}(*args, &block) Xml.#{method}(self, *args); end})
17
+ end
18
+
19
+ alias :[] :attr
20
+ end
21
+
22
+
23
+ module_function
24
+
25
+ def parse(str, namespaces={})
26
+ Element.new(Nokogiri::XML(str), namespaces)
27
+ end
28
+
29
+ def inner_text(element, xpath=nil)
30
+ return inner_text(select(element, xpath)) if xpath
31
+ return nil if attr(element, "nil") && attr(element, "nil") == "true"
32
+ element.node.inner_text
33
+ end
34
+
35
+ def optional_inner_text(parent_element, xpath)
36
+ element = select(parent_element, xpath)
37
+ element.node.nil? ? nil : element.inner_text
38
+ end
39
+
40
+ def select(element, xpath)
41
+ Element.new(element.node.at(xpath, element.namespaces), element.namespaces)
42
+ end
43
+
44
+ def select_all(element, xpath)
45
+ element.node.search(xpath, element.namespaces).map { |n| Element.new(n, element.namespaces) }
46
+ end
47
+
48
+ def attr(element, attr_name)
49
+ raise 'element selection is empty!' if element.nil?
50
+ element.node[attr_name]
51
+ end
52
+
53
+ def children(element)
54
+ element.node.children.select { |e| e.is_a?(Nokogiri::XML::Element) }.map { |n| Element.new(n, element.namespaces) }
55
+ end
56
+
57
+ def tag_name(element)
58
+ element.node.name
59
+ end
60
+
61
+ def raw_xml(element)
62
+ patching_namespaces(element.node).to_s
63
+ end
64
+
65
+ def attributes(element)
66
+ element.node.attribute_nodes.inject({}) do |memo, a|
67
+ memo[a.name] = a.value
68
+ memo
69
+ end
70
+ end
71
+
72
+ def to_hash(element, hash={})
73
+ hash_for_element = (hash[tag_name(element).to_sym] ||= {})
74
+
75
+ attributes(element).each do |name, value|
76
+ hash_for_element[name.to_sym] = value
77
+ end
78
+
79
+ children(element).each do |child|
80
+ if children(child).any?
81
+ to_hash(child, hash_for_element)
82
+ else
83
+ hash_for_element[tag_name(child).to_sym] = inner_text(child)
84
+ end
85
+ end
86
+
87
+ hash
88
+ end
89
+
90
+ def patching_namespaces(node)
91
+ ns_scopes = node.namespace_scopes
92
+ return node if ns_scopes.empty?
93
+
94
+ result = node.clone
95
+ ns_scopes.each do |ns|
96
+ result.add_namespace_definition(ns.prefix, ns.href)
97
+ end
98
+ result
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,98 @@
1
+ module MingleEvents
2
+ class ZipDirectory
3
+
4
+ def initialize(name)
5
+ FileUtils.mkdir_p(File.dirname(name))
6
+ @root = name
7
+ @entry_map = nil
8
+ @readio = nil
9
+ end
10
+
11
+ def write_file(path, &block)
12
+ measure('write_file') do
13
+ exists = File.exists?(@root) && File.lstat(@root).size > 1024
14
+ writeio = File.open(@root, exists ? 'r+b' : 'wb')
15
+ # for a existing tar, seek to -1024 from end to skip 1024 '\0' in tar format
16
+ writeio.seek(-1024, IO::SEEK_END) if exists
17
+
18
+ Archive::Tar::Minitar::Output.open(writeio) do |output|
19
+ stat = {:mode => 0100644, :mtime => Time.now}
20
+ output.tar.add_file(path, stat) { |io, opts| yield(io) }
21
+ end
22
+ end
23
+ end
24
+
25
+ def file(path, &block)
26
+ measure("read file") do
27
+ raise "File at '#{path}' in archive '#{@root}' dose not exisits" unless exists?(path)
28
+ entry_map[path].open { |entry_stream| yield(entry_stream) }
29
+ end
30
+ end
31
+
32
+ def exists?(path)
33
+ return unless File.exists?(@root)
34
+ entry_map.include?(path)
35
+ end
36
+
37
+ def delete
38
+ close
39
+ FileUtils.rm_rf(@root)
40
+ end
41
+
42
+ def close
43
+ @readio.close if @readio
44
+ @readio = nil
45
+ @entry_map = nil
46
+ end
47
+
48
+ alias :reload :close
49
+
50
+ private
51
+
52
+ class ReusableEntryStream < Archive::Tar::Minitar::Reader::EntryStream
53
+ def open(&block)
54
+ rewind
55
+ yield(self)
56
+ end
57
+
58
+ def close
59
+ # do nothing
60
+ end
61
+
62
+ def external_encoding
63
+ "UTF-8"
64
+ end
65
+ end
66
+
67
+ def entry_map
68
+ return @entry_map if @entry_map
69
+
70
+ @entry_map = {}
71
+ measure("entries archive loading") do
72
+ @readio = File.open(@root, 'rb')
73
+ loop do
74
+ break if @readio.eof?
75
+
76
+ header = Archive::Tar::PosixHeader.new_from_stream(@readio)
77
+ break if header.empty?
78
+
79
+ entry = ReusableEntryStream.new(header, @readio)
80
+ size = entry.size
81
+ @entry_map[entry.name] = entry
82
+
83
+ skip = (512 - (size % 512)) % 512
84
+ @readio.seek(size - entry.bytes_read, IO::SEEK_CUR)
85
+ @readio.read(skip) # discard trailing zeros
86
+ end
87
+ @readio.rewind
88
+ end
89
+ @entry_map
90
+ end
91
+
92
+ def measure(label=nil, &block)
93
+ return yield unless ENV['MINGLE_EVENTS_VERBOSE']
94
+ start = Time.now
95
+ yield.tap { puts "ZipDirectory##{label}: using #{Time.now - start}s"}
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,75 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ class EntryCacheTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @entry_cache = EntryCache.new("/tmp/foo")
8
+ @entry_cache.clear
9
+ end
10
+
11
+ def test_stores_first_entry_under_root
12
+ fetched_entry = entry(1)
13
+ @entry_cache.write(fetched_entry, nil)
14
+ @entry_cache.update_current_state(fetched_entry, fetched_entry)
15
+ assert_equal [entry(1)], @entry_cache.all_entries.to_a
16
+ assert_equal entry(1), @entry_cache.first
17
+ assert_equal entry(1), @entry_cache.latest
18
+ end
19
+
20
+ def test_store_multiple_entries
21
+ @entry_cache.write(entry(3), nil)
22
+ @entry_cache.write(entry(2), entry(3))
23
+ @entry_cache.write(entry(1), entry(2))
24
+ @entry_cache.update_current_state(entry(1), entry(3))
25
+ assert_equal [entry(1), entry(2), entry(3)], @entry_cache.all_entries.to_a
26
+ end
27
+
28
+ def test_store_multiple_entries_multiple_times
29
+ @entry_cache.write(entry(3), nil)
30
+ @entry_cache.write(entry(2), entry(3))
31
+ @entry_cache.write(entry(1), entry(2))
32
+ @entry_cache.update_current_state(entry(1), entry(3))
33
+
34
+ @entry_cache.write(entry(6), nil)
35
+ @entry_cache.write(entry(5), entry(6))
36
+ @entry_cache.write(entry(4), entry(5))
37
+ @entry_cache.write(entry(3), entry(4))
38
+ @entry_cache.update_current_state(entry(3), entry(6))
39
+
40
+ assert_equal((1..6).map { |id| entry(id) }, @entry_cache.all_entries.to_a)
41
+ end
42
+
43
+ def test_entries_returned_can_be_lazily_evaluated
44
+ @entry_cache.write(entry(3), nil)
45
+ @entry_cache.write(entry(2), entry(3))
46
+ @entry_cache.write(entry(1), entry(2))
47
+ @entry_cache.update_current_state(entry(1), entry(3))
48
+ enumerator = @entry_cache.all_entries.each
49
+ assert_equal entry(1), enumerator.next()
50
+ assert_equal entry(2), enumerator.next()
51
+ assert_equal entry(3), enumerator.next()
52
+ assert_raise(StopIteration) { enumerator.next() }
53
+ end
54
+
55
+ private
56
+
57
+ def entry(id)
58
+ element_xml_text = %{
59
+ <entry xmlns="http://www.w3.org/2005/Atom">
60
+ <id>https://mingle.example.com/projects/mingle/events/index/#{id}</id>
61
+ <title>Page Special:HeaderActions changed</title>
62
+ <updated>2011-02-03T08:12:42Z</updated>
63
+ <author>
64
+ <name>Sammy Soso</name>
65
+ <email>sammy@example.com</email>
66
+ <uri>https://mingle.example.com/api/v2/users/233.xml</uri>
67
+ </author>
68
+ </entry>
69
+ }
70
+ Feed::Entry.from_snippet(element_xml_text)
71
+ end
72
+
73
+
74
+ end
75
+ end
@@ -7,7 +7,7 @@ module MingleEvents
7
7
 
8
8
  def test_parse_attributes
9
9
  author_xml = %{
10
- <author >
10
+ <author xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
11
11
  <name>Sammy Soso</name>
12
12
  <email>sammy@example.com</email>
13
13
  <uri>https://mingle.example.com/api/v2/users/233.xml</uri>
@@ -23,7 +23,7 @@ module MingleEvents
23
23
 
24
24
  def test_parse_attributes_when_no_optional_fields
25
25
  author_xml = %{
26
- <author>
26
+ <author xmlns="http://www.w3.org/2005/Atom" xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
27
27
  <name>Sammy Soso</name>
28
28
  </author>
29
29
  }
@@ -7,9 +7,9 @@ module MingleEvents
7
7
 
8
8
  def test_parse_multiple_changes
9
9
  element_xml_text = %{
10
- <entry>
10
+ <entry xmlns="http://www.w3.org/2005/Atom">
11
11
  <content type="application/vnd.mingle+xml">
12
- <changes>
12
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
13
13
  <change type="card-creation"/>
14
14
  <change type="card-type-change">
15
15
  <old_value nil="true"></old_value>
@@ -41,13 +41,13 @@ module MingleEvents
41
41
 
42
42
  def test_parse_name_change_from_nil
43
43
  element_xml_text = %{
44
- <entry>
44
+ <entry xmlns="http://www.w3.org/2005/Atom">
45
45
  <content type="application/vnd.mingle+xml">
46
- <changes>
46
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
47
47
  <change type="name-change">
48
48
  <old_value nil="true" />
49
49
  <new_value>Basic email integration</new_value>
50
- </change
50
+ </change>
51
51
  </changes>
52
52
  </content>
53
53
  </entry>}
@@ -62,9 +62,9 @@ module MingleEvents
62
62
 
63
63
  def test_parse_name_change
64
64
  element_xml_text = %{
65
- <entry>
65
+ <entry xmlns="http://www.w3.org/2005/Atom">
66
66
  <content type="application/vnd.mingle+xml">
67
- <changes>
67
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
68
68
  <change type="name-change">
69
69
  <old_value>Work with email</old_value>
70
70
  <new_value>Basic email integration</new_value>
@@ -83,9 +83,9 @@ module MingleEvents
83
83
 
84
84
  def test_parse_type_info_when_no_custom_builder_specified
85
85
  element_xml_text = %{
86
- <entry>
86
+ <entry xmlns="http://www.w3.org/2005/Atom">
87
87
  <content type="application/vnd.mingle+xml">
88
- <changes>
88
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
89
89
  <change type="card-creation"/>
90
90
  </changes>
91
91
  </content>
@@ -98,9 +98,9 @@ module MingleEvents
98
98
 
99
99
  def test_parse_card_type_change_from_nil
100
100
  element_xml_text = %{
101
- <entry>
101
+ <entry xmlns="http://www.w3.org/2005/Atom">
102
102
  <content type="application/vnd.mingle+xml">
103
- <changes>
103
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
104
104
  <change type="card-type-change">
105
105
  <old_value nil="true"></old_value>
106
106
  <new_value>
@@ -125,13 +125,13 @@ module MingleEvents
125
125
 
126
126
  def test_parse_card_type_change
127
127
  element_xml_text = %{
128
- <entry>
128
+ <entry xmlns="http://www.w3.org/2005/Atom">
129
129
  <content type="application/vnd.mingle+xml">
130
- <changes>
130
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
131
131
  <change type="card-type-change">
132
132
  <old_value>
133
133
  <card_type url="https://mingle.example.com/api/v2/projects/atlas/card_types/30.xml">
134
- <name>Story</name
134
+ <name>Story</name>
135
135
  </card_type>
136
136
  </old_value>
137
137
  <new_value>
@@ -156,9 +156,9 @@ module MingleEvents
156
156
 
157
157
  def test_parse_card_type_change_to_deleted_type
158
158
  element_xml_text = %{
159
- <entry>
159
+ <entry xmlns="http://www.w3.org/2005/Atom">
160
160
  <content type="application/vnd.mingle+xml">
161
- <changes>
161
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
162
162
  <change type="card-type-change">
163
163
  <old_value>
164
164
  <card_type url="https://mingle.example.com/api/v2/projects/atlas/card_types/30.xml">
@@ -187,9 +187,9 @@ module MingleEvents
187
187
 
188
188
  def test_parse_card_property_change
189
189
  element_xml_text = %{
190
- <entry>
190
+ <entry xmlns="http://www.w3.org/2005/Atom">
191
191
  <content type="application/vnd.mingle+xml">
192
- <changes>
192
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
193
193
  <change type="property-change">
194
194
  <property_definition
195
195
  url="http://mingle.example.com/api/v2/projects/atlas/property_definitions/10418.xml">
@@ -220,9 +220,9 @@ module MingleEvents
220
220
 
221
221
  def test_parse_card_property_change_from_nil
222
222
  element_xml_text = %{
223
- <entry>
223
+ <entry xmlns="http://www.w3.org/2005/Atom">
224
224
  <content type="application/vnd.mingle+xml">
225
- <changes>
225
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
226
226
  <change type="property-change">
227
227
  <property_definition
228
228
  url="http://mingle.example.com/api/v2/projects/atlas/property_definitions/10418.xml">
@@ -246,9 +246,9 @@ module MingleEvents
246
246
 
247
247
  def test_parse_card_property_change_to_nil
248
248
  element_xml_text = %{
249
- <entry>
249
+ <entry xmlns="http://www.w3.org/2005/Atom">
250
250
  <content type="application/vnd.mingle+xml">
251
- <changes>
251
+ <changes xmlns="http://www.thoughtworks-studios.com/ns/mingle">
252
252
  <change type="property-change">
253
253
  <property_definition
254
254
  url="http://mingle.example.com/api/v2/projects/atlas/property_definitions/10418.xml">
@@ -7,7 +7,7 @@ module MingleEvents
7
7
 
8
8
  def test_parse_basic_attributes
9
9
  element_xml_text = %{
10
- <entry>
10
+ <entry xmlns="http://www.w3.org/2005/Atom" >
11
11
  <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
12
12
  <title>Page Special:HeaderActions changed</title>
13
13
  <updated>2011-02-03T08:12:42Z</updated>
@@ -22,13 +22,13 @@ module MingleEvents
22
22
  # assert_equal(element_xml_text.inspect, entry.raw_xml.inspect)
23
23
  assert_equal("https://mingle.example.com/projects/mingle/events/index/234443", entry.entry_id)
24
24
  assert_equal("Page Special:HeaderActions changed", entry.title)
25
- assert_equal("Thu Feb 03 08:12:42 UTC 2011", entry.updated.to_s)
25
+ assert_equal(Time.utc(2011, 2, 3, 8, 12, 42), entry.updated)
26
26
  assert_equal("Sammy Soso", entry.author.name)
27
27
  end
28
28
 
29
29
  def test_parse_categories
30
30
  element_xml_text = %{
31
- <entry>
31
+ <entry xmlns="http://www.w3.org/2005/Atom">
32
32
  <category term="foo" scheme='http://tws.com/ns#mingle' />
33
33
  <category term="bar" scheme="http://tws.com/ns#go" />
34
34
  </entry>}
@@ -45,7 +45,7 @@ module MingleEvents
45
45
  # that the card number is derived from a single, precise position
46
46
 
47
47
  element_xml_text = %{
48
- <entry>
48
+ <entry xmlns="http://www.w3.org/2005/Atom">
49
49
  <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
50
50
  <link href="https://mingle.example.com/projects/atlas/cards/102" rel="http://www.thoughtworks-studios.com/ns/mingle#event-source" type="text/html" title="bug #103"/>
51
51
  <link href="https://mingle.example.com/api/v2/projects/atlas/cards/104.xml?version=7" rel="http://www.thoughtworks-studios.com/ns/mingle#version" type="application/vnd.mingle+xml" title="bug #105 (v7)"/>
@@ -59,7 +59,7 @@ module MingleEvents
59
59
 
60
60
  def test_card_number_and_version_throws_error_when_event_not_related_to_a_card
61
61
  element_xml_text = %{
62
- <entry>
62
+ <entry xmlns="http://www.w3.org/2005/Atom">
63
63
  <category term="page" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
64
64
  </entry>}
65
65
  entry = Entry.from_snippet(element_xml_text)
@@ -86,7 +86,7 @@ module MingleEvents
86
86
  # that the card number is derived from a single, precise position
87
87
 
88
88
  element_xml_text = %{
89
- <entry>
89
+ <entry xmlns="http://www.w3.org/2005/Atom">
90
90
  <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
91
91
  <link href="https://mingle.example.com/projects/atlas/cards/102" rel="http://www.thoughtworks-studios.com/ns/mingle#event-source" type="text/html" title="bug #103"/>
92
92
  <link href="https://mingle.example.com/api/v2/projects/atlas/cards/104.xml?version=7" rel="http://www.thoughtworks-studios.com/ns/mingle#version" type="application/vnd.mingle+xml" title="bug #105 (v7)"/>
@@ -99,7 +99,7 @@ module MingleEvents
99
99
 
100
100
  def test_card_version_resource_uri_throws_error_when_not_card_event
101
101
  element_xml_text = %{
102
- <entry>
102
+ <entry xmlns="http://www.w3.org/2005/Atom">
103
103
  <category term="page" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
104
104
  </entry>}
105
105
  entry = Entry.from_snippet(element_xml_text)
@@ -113,7 +113,7 @@ module MingleEvents
113
113
 
114
114
  def test_entry_id_aliased_as_event_id
115
115
  element_xml_text = %{
116
- <entry>
116
+ <entry xmlns="http://www.w3.org/2005/Atom">
117
117
  <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
118
118
  </entry>}
119
119
  entry = Entry.from_snippet(element_xml_text)
@@ -123,21 +123,21 @@ module MingleEvents
123
123
 
124
124
  def test_entry_id_determines_equality
125
125
  element_xml_text_1 = %{
126
- <entry>
126
+ <entry xmlns="http://www.w3.org/2005/Atom">
127
127
  <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
128
128
  <category term="page" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
129
129
  </entry>}
130
130
  entry_1 = Entry.from_snippet(element_xml_text_1)
131
131
 
132
132
  element_xml_text_2 = %{
133
- <entry>
133
+ <entry xmlns="http://www.w3.org/2005/Atom">
134
134
  <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
135
135
  <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
136
136
  </entry>}
137
137
  entry_2 = Entry.from_snippet(element_xml_text_2)
138
138
 
139
139
  element_xml_text_3 = %{
140
- <entry>
140
+ <entry xmlns="http://www.w3.org/2005/Atom">
141
141
  <id>https://mingle.example.com/projects/mingle/events/index/234</id>
142
142
  <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
143
143
  </entry>}
@@ -151,7 +151,7 @@ module MingleEvents
151
151
 
152
152
  def test_construct_links
153
153
  element_xml_text = %{
154
- <entry>
154
+ <entry xmlns="http://www.w3.org/2005/Atom">
155
155
  <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
156
156
  <link href="https://mingle.example.com/projects/atlas/cards/102" rel="http://www.thoughtworks-studios.com/ns/mingle#event-source" type="text/html" title="bug #103"/>
157
157
  <link href="https://mingle.example.com/api/v2/projects/atlas/cards/104.xml?version=7" rel="http://www.thoughtworks-studios.com/ns/mingle#version" type="application/vnd.mingle+xml" title="bug #105 (v7)"/>