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.
- 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
|