mingle_events 0.1.3 → 0.1.4
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/Gemfile +2 -4
- data/lib/mingle_events.rb +19 -3
- data/lib/mingle_events/entry_cache.rb +129 -0
- data/lib/mingle_events/feed/author.rb +12 -23
- data/lib/mingle_events/feed/category.rb +2 -0
- data/lib/mingle_events/feed/changes.rb +19 -50
- data/lib/mingle_events/feed/entry.rb +16 -21
- data/lib/mingle_events/feed/links.rb +3 -3
- data/lib/mingle_events/feed/page.rb +5 -5
- data/lib/mingle_events/http.rb +50 -0
- data/lib/mingle_events/mingle_basic_auth_access.rb +17 -50
- data/lib/mingle_events/mingle_hmac_auth_access.rb +22 -0
- data/lib/mingle_events/mingle_oauth_access.rb +10 -30
- data/lib/mingle_events/poller.rb +4 -3
- data/lib/mingle_events/processors/author_filter.rb +7 -7
- data/lib/mingle_events/processors/card_data.rb +15 -15
- data/lib/mingle_events/project_custom_properties.rb +15 -15
- data/lib/mingle_events/project_event_fetcher.rb +21 -110
- data/lib/mingle_events/xml.rb +101 -0
- data/lib/mingle_events/zip_directory.rb +98 -0
- data/test/mingle_events/entry_cache_test.rb +75 -0
- data/test/mingle_events/feed/author_test.rb +2 -2
- data/test/mingle_events/feed/changes_test.rb +22 -22
- data/test/mingle_events/feed/entry_test.rb +12 -12
- data/test/mingle_events/feed/links_test.rb +5 -5
- data/test/mingle_events/mingle_basic_auth_access_test.rb +15 -0
- data/test/mingle_events/mingle_hmac_auth_access_test.rb +15 -0
- data/test/mingle_events/mingle_oauth_access_test.rb +15 -0
- data/test/mingle_events/poller_test.rb +1 -1
- data/test/mingle_events/processors/author_filter_test.rb +5 -5
- data/test/mingle_events/processors/card_data_test.rb +12 -17
- data/test/mingle_events/project_custom_properties_test.rb +2 -3
- data/test/mingle_events/project_event_fetcher_test.rb +64 -8
- data/test/mingle_events/xml_test.rb +80 -0
- data/test/mingle_events/zip_directory_test.rb +44 -0
- data/test/test_helper.rb +36 -27
- metadata +121 -62
data/Gemfile
CHANGED
data/lib/mingle_events.rb
CHANGED
|
@@ -6,21 +6,37 @@ require 'logger'
|
|
|
6
6
|
|
|
7
7
|
require 'rubygems'
|
|
8
8
|
require 'nokogiri'
|
|
9
|
+
require 'active_support'
|
|
10
|
+
require 'active_support/core_ext'
|
|
11
|
+
require 'archive/tar/minitar'
|
|
12
|
+
require 'api_auth'
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'feed'))
|
|
11
|
-
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', '
|
|
16
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'xml'))
|
|
12
17
|
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'poller'))
|
|
18
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'http_error'))
|
|
19
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'http'))
|
|
13
20
|
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'mingle_basic_auth_access'))
|
|
14
21
|
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'mingle_oauth_access'))
|
|
22
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'mingle_hmac_auth_access'))
|
|
15
23
|
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'processors'))
|
|
16
24
|
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'project_custom_properties'))
|
|
25
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'zip_directory'))
|
|
26
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'entry_cache'))
|
|
17
27
|
require File.expand_path(File.join(File.dirname(__FILE__), 'mingle_events', 'project_event_fetcher'))
|
|
18
28
|
|
|
19
29
|
module MingleEvents
|
|
20
|
-
|
|
30
|
+
|
|
21
31
|
attr_accessor :log
|
|
22
32
|
module_function :log, :log=
|
|
23
33
|
self.log = Logger.new(STDOUT)
|
|
24
34
|
self.log.level = Logger::INFO
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
|
|
37
|
+
URIParser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
|
38
|
+
ATOM_AND_MINGLE_NS = {
|
|
39
|
+
'atom' => "http://www.w3.org/2005/Atom",
|
|
40
|
+
'mingle' => "http://www.thoughtworks-studios.com/ns/mingle"
|
|
41
|
+
}
|
|
42
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module MingleEvents
|
|
2
|
+
class EntryCache
|
|
3
|
+
def initialize(root_dir)
|
|
4
|
+
@dir = ZipDirectory.new(root_dir)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def all_entries
|
|
8
|
+
current_state = load_current_state
|
|
9
|
+
Entries.new(@dir, current_state[:first_fetched_entry_info_file], current_state[:last_fetched_entry_info_file])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def entries(from_entry, to_entry)
|
|
13
|
+
Entries.new(@dir, file_for_entry(from_entry), file_for_entry(to_entry))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def first
|
|
17
|
+
current_state_entry(:first_fetched_entry_info_file)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def latest
|
|
21
|
+
current_state_entry(:last_fetched_entry_info_file)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write(entry, next_entry)
|
|
25
|
+
file = file_for_entry(entry)
|
|
26
|
+
file_content = {:entry_xml => entry.raw_xml, :next_entry_file_path => file_for_entry(next_entry)}
|
|
27
|
+
@dir.write_file(file) {|out| YAML.dump(file_content, out)}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def has_current_state?
|
|
31
|
+
@dir.exists?(current_state_file)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def set_current_state(latest_entry)
|
|
35
|
+
return if latest_entry.nil?
|
|
36
|
+
write(latest_entry, nil)
|
|
37
|
+
update_current_state(latest_entry, latest_entry)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update_current_state(oldest_new_entry, most_recent_new_entry)
|
|
41
|
+
current_state = load_current_state
|
|
42
|
+
current_state.merge!(:last_fetched_entry_info_file => file_for_entry(most_recent_new_entry))
|
|
43
|
+
if current_state[:first_fetched_entry_info_file].nil?
|
|
44
|
+
current_state.merge!(:first_fetched_entry_info_file => file_for_entry(oldest_new_entry))
|
|
45
|
+
end
|
|
46
|
+
@dir.write_file(current_state_file) { |out| YAML.dump(current_state, out) }
|
|
47
|
+
@dir.reload
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear
|
|
51
|
+
@dir.delete
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def load_current_state
|
|
57
|
+
if has_current_state?
|
|
58
|
+
@dir.file(current_state_file) { |f| YAML.load(f)}
|
|
59
|
+
else
|
|
60
|
+
{:last_fetched_entry_info_file => nil, :first_fetched_entry_info_file => nil}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def current_state_file
|
|
65
|
+
'current_state.yml'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def current_state_entry(info_file_key)
|
|
69
|
+
if info_file = load_current_state[info_file_key]
|
|
70
|
+
Feed::Entry.from_snippet((@dir.file(info_file) { |f| YAML.load(f) })[:entry_xml])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def file_for_entry(entry)
|
|
75
|
+
return nil if entry.nil?
|
|
76
|
+
entry_id_as_uri = URI.parse(entry.entry_id)
|
|
77
|
+
relative_path_parts = entry_id_as_uri.path.split('/').reject(&:blank?)
|
|
78
|
+
entry_id_int = relative_path_parts.last
|
|
79
|
+
insertions = ["#{entry_id_int.to_i/16384}", "#{entry_id_int.to_i%16384}"]
|
|
80
|
+
relative_path_parts = relative_path_parts[0..-2] + insertions + ["#{entry_id_int}.yml"]
|
|
81
|
+
File.join(*relative_path_parts)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class Entries
|
|
85
|
+
include Enumerable
|
|
86
|
+
|
|
87
|
+
class Enumerator
|
|
88
|
+
def initialize(dir, first, last)
|
|
89
|
+
@dir, @first, @last = dir, first, last
|
|
90
|
+
@last_pos = :limbo
|
|
91
|
+
@current_pos = first
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def next
|
|
95
|
+
raise StopIteration.new unless has_next?
|
|
96
|
+
current_entry_info = @dir.file(@current_pos) {|f| YAML.load(f) }
|
|
97
|
+
Feed::Entry.from_snippet(current_entry_info[:entry_xml]).tap do
|
|
98
|
+
@last_pos = @current_pos
|
|
99
|
+
@current_pos = current_entry_info[:next_entry_file_path]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
def has_next?
|
|
105
|
+
@current_pos && @last_pos != @last
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def initialize(state_dir, first_info_file, last_info_file)
|
|
110
|
+
@dir = state_dir
|
|
111
|
+
@first_info_file = first_info_file
|
|
112
|
+
@last_info_file = last_info_file
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def each(&block)
|
|
116
|
+
enumerator = Enumerator.new(@dir, @first_info_file, @last_info_file)
|
|
117
|
+
return enumerator unless block_given?
|
|
118
|
+
loop do
|
|
119
|
+
begin
|
|
120
|
+
yield(enumerator.next)
|
|
121
|
+
rescue StopIteration
|
|
122
|
+
break
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
module MingleEvents
|
|
1
|
+
module MingleEvents
|
|
2
2
|
module Feed
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
# The user who's Mingle activity triggered this event
|
|
5
5
|
class Author
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
# The name of the author
|
|
8
8
|
attr_reader :name
|
|
9
9
|
# The email address of the author
|
|
@@ -12,29 +12,18 @@ module MingleEvents
|
|
|
12
12
|
attr_reader :uri
|
|
13
13
|
# The URI for the author's icon
|
|
14
14
|
attr_reader :icon_uri
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
def initialize(author_element)
|
|
17
|
-
@name = author_element.
|
|
18
|
-
@email =
|
|
19
|
-
@uri =
|
|
20
|
-
@icon_uri =
|
|
17
|
+
@name = author_element.inner_text("./atom:name")
|
|
18
|
+
@email = author_element.optional_inner_text("./atom:email")
|
|
19
|
+
@uri = author_element.optional_inner_text("./atom:uri")
|
|
20
|
+
@icon_uri = author_element.optional_inner_text("./mingle:icon")
|
|
21
21
|
end
|
|
22
|
-
|
|
23
|
-
def self.from_snippet(
|
|
24
|
-
self.new(
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def optional_element_text(parent_element, element_name)
|
|
30
|
-
element = parent_element.at(element_name)
|
|
31
|
-
element.nil? ? nil : element.inner_text
|
|
22
|
+
|
|
23
|
+
def self.from_snippet(author_xml)
|
|
24
|
+
self.new(Xml.parse(author_xml, ATOM_AND_MINGLE_NS).select("/atom:author"))
|
|
32
25
|
end
|
|
33
|
-
|
|
34
26
|
end
|
|
35
|
-
|
|
27
|
+
|
|
36
28
|
end
|
|
37
29
|
end
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
@@ -47,6 +47,8 @@ module MingleEvents
|
|
|
47
47
|
CARD = Category.new('card', MINGLE_SCHEME)
|
|
48
48
|
# Category for any event that is the creation of a new card
|
|
49
49
|
CARD_CREATION = Category.new('card-creation', MINGLE_SCHEME)
|
|
50
|
+
# Category for any event that is the copy of a card from one project to another
|
|
51
|
+
CARD_COPIED_FROM = Category.new('card-copied-from', MINGLE_SCHEME)
|
|
50
52
|
# Category for any event that is the deletion of a card
|
|
51
53
|
CARD_DELETION = Category.new('card-deletion', MINGLE_SCHEME)
|
|
52
54
|
# Category for any event that includes the change of a card's property value
|
|
@@ -1,76 +1,45 @@
|
|
|
1
|
-
module MingleEvents
|
|
1
|
+
module MingleEvents
|
|
2
2
|
module Feed
|
|
3
|
-
|
|
4
|
-
# Enumerable detail for each change specified in the entry's content section
|
|
3
|
+
|
|
4
|
+
# Enumerable detail for each change specified in the entry's content section
|
|
5
5
|
class Changes
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
include Enumerable
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
def initialize(changes_element)
|
|
10
10
|
@changes_element = changes_element
|
|
11
11
|
end
|
|
12
|
-
|
|
13
|
-
def each
|
|
12
|
+
|
|
13
|
+
def each
|
|
14
14
|
(@changes ||= parse_changes).each{|c| yield c}
|
|
15
15
|
end
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
private
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def parse_changes
|
|
20
20
|
changes = []
|
|
21
|
-
@changes_element.
|
|
21
|
+
@changes_element.select_all("./mingle:change").map do |change_element|
|
|
22
22
|
category = Category.for_mingle_term(change_element["type"])
|
|
23
23
|
changes << Change.new(category).build(change_element)
|
|
24
24
|
end
|
|
25
25
|
changes
|
|
26
26
|
end
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
class Change
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
def initialize(category)
|
|
31
31
|
@category = category
|
|
32
32
|
end
|
|
33
|
-
|
|
34
|
-
def build(element)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
raw_hash_from_xml = element_to_hash(element)
|
|
38
|
-
|
|
33
|
+
|
|
34
|
+
def build(element)
|
|
35
|
+
raw_hash_from_xml = element.to_hash
|
|
36
|
+
|
|
39
37
|
raw_hash_from_xml[:change].merge({
|
|
40
|
-
:category => @category,
|
|
38
|
+
:category => @category,
|
|
41
39
|
:type => @category
|
|
42
40
|
})
|
|
43
41
|
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def element_to_hash(element, hash = {})
|
|
48
|
-
hash_for_element = (hash[element.name.to_sym] ||= {})
|
|
49
|
-
|
|
50
|
-
element.attribute_nodes.each do |a|
|
|
51
|
-
hash_for_element[a.name.to_sym] = a.value
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
element.children.each do |child|
|
|
55
|
-
next unless child.is_a?(Nokogiri::XML::Element)
|
|
56
|
-
|
|
57
|
-
if child.children.count{|c| c.is_a?(Nokogiri::XML::Element)} > 0
|
|
58
|
-
element_to_hash(child, hash_for_element)
|
|
59
|
-
else
|
|
60
|
-
hash_for_element[child.name.to_sym] = if child["nil"] && child["nil"] == "true"
|
|
61
|
-
nil
|
|
62
|
-
else
|
|
63
|
-
child.inner_text
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
hash
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
75
44
|
end
|
|
76
45
|
end
|
|
@@ -5,46 +5,46 @@ module MingleEvents
|
|
|
5
5
|
# representing an event in Mingle.
|
|
6
6
|
class Entry
|
|
7
7
|
|
|
8
|
-
# Construct with the wrapped
|
|
8
|
+
# Construct with the wrapped Xml Elem for the entry
|
|
9
9
|
def initialize(entry_element)
|
|
10
10
|
@entry_element = entry_element
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def self.from_snippet(entry_xml)
|
|
14
|
-
self.new(
|
|
14
|
+
self.new(Xml.parse(entry_xml, ATOM_AND_MINGLE_NS).select('/atom:entry'))
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# The raw entry XML from the Atom feed
|
|
18
18
|
def raw_xml
|
|
19
|
-
@raw_xml ||= @entry_element.
|
|
19
|
+
@raw_xml ||= @entry_element.raw_xml
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# The Atom entry's id value. This is the one true identifier for the entry,
|
|
23
23
|
# and therefore the event.
|
|
24
24
|
def entry_id
|
|
25
|
-
@entry_id ||= @entry_element.
|
|
25
|
+
@entry_id ||= @entry_element.inner_text("./atom:id")
|
|
26
26
|
end
|
|
27
27
|
alias :event_id :entry_id
|
|
28
28
|
|
|
29
29
|
# The Atom entry's title
|
|
30
30
|
def title
|
|
31
|
-
@title ||= @entry_element.
|
|
31
|
+
@title ||= @entry_element.inner_text('./atom:title')
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# The time at which entry was created, i.e., the event was triggered
|
|
35
35
|
def updated
|
|
36
|
-
@updated ||= Time.parse(@entry_element.
|
|
36
|
+
@updated ||= Time.parse(@entry_element.inner_text("./atom:updated"))
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# The user who created the entry (triggered the event), i.e., changed project data in Mingle
|
|
40
|
-
def author
|
|
41
|
-
@author ||= Author.new(@entry_element.
|
|
40
|
+
def author
|
|
41
|
+
@author ||= Author.new(@entry_element.select("./atom:author"))
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# The set of Atom categoies describing the entry
|
|
45
45
|
def categories
|
|
46
|
-
@categories ||= @entry_element.
|
|
47
|
-
Category.new(category_element
|
|
46
|
+
@categories ||= @entry_element.select_all("./atom:category").map do |category_element|
|
|
47
|
+
Category.new(category_element.attr("term"), category_element.attr("scheme"))
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -55,7 +55,7 @@ module MingleEvents
|
|
|
55
55
|
# The data in the change hashes reflect only what is in the XML as encriching them would
|
|
56
56
|
# require potentially many calls to the Mingle server resulting in very slow processing.
|
|
57
57
|
def changes
|
|
58
|
-
@changes ||= Changes.new(@entry_element.
|
|
58
|
+
@changes ||= Changes.new(@entry_element.select("./atom:content/mingle:changes"))
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Whether the entry/event was sourced by a Mingle card
|
|
@@ -108,21 +108,16 @@ module MingleEvents
|
|
|
108
108
|
private
|
|
109
109
|
|
|
110
110
|
def parse_card_number
|
|
111
|
-
card_number_element = @entry_element.
|
|
112
|
-
"link[@rel='http://www.thoughtworks-studios.com/ns/mingle#event-source'][@type='application/vnd.mingle+xml']"
|
|
113
|
-
)
|
|
111
|
+
card_number_element = @entry_element.select("./atom:link[@rel='http://www.thoughtworks-studios.com/ns/mingle#event-source'][@type='application/vnd.mingle+xml']")
|
|
114
112
|
# TODO: improve this bit of parsing :)
|
|
115
|
-
card_number_element
|
|
113
|
+
card_number_element.attr("href").split('/').last.split('.')[0..-2].join.to_i
|
|
116
114
|
end
|
|
117
115
|
|
|
118
116
|
def parse_card_version_resource_uri
|
|
119
|
-
card_number_element = @entry_element.
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
card_number_element["href"]
|
|
117
|
+
card_number_element = @entry_element.select("./atom:link[@rel='http://www.thoughtworks-studios.com/ns/mingle#version'][@type='application/vnd.mingle+xml']")
|
|
118
|
+
card_number_element.attr("href")
|
|
123
119
|
end
|
|
124
|
-
|
|
125
120
|
end
|
|
126
121
|
|
|
127
122
|
end
|
|
128
|
-
end
|
|
123
|
+
end
|
|
@@ -10,8 +10,8 @@ module MingleEvents
|
|
|
10
10
|
RELATED_REL = "http://www.thoughtworks-studios.com/ns/mingle#related"
|
|
11
11
|
|
|
12
12
|
def initialize(entry_element)
|
|
13
|
-
@links ||= entry_element.
|
|
14
|
-
Link.new(
|
|
13
|
+
@links ||= entry_element.select_all("./atom:link").map do |link_element|
|
|
14
|
+
Link.new(*%w(href rel type title).map { |name| link_element.attr(name) })
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -39,4 +39,4 @@ module MingleEvents
|
|
|
39
39
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
end
|
|
42
|
+
end
|