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.
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
data/Gemfile CHANGED
@@ -1,4 +1,2 @@
1
- source 'http://rubygems.org'
2
-
3
- gem 'nokogiri'
4
- gem 'activesupport'
1
+ source :rubygems
2
+ gemspec
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', 'http_error'))
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
- end
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.at("name").inner_text
18
- @email = optional_element_text(author_element, 'email')
19
- @uri = optional_element_text(author_element, 'uri')
20
- @icon_uri = optional_element_text(author_element, 'icon')
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(entry_xml)
24
- self.new(Nokogiri::XML(entry_xml))
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.search("change").map do |change_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
- element_to_hash(element)
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
- private
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 Nokogiri Elem for the entry
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(Nokogiri::XML(entry_xml).remove_namespaces!)
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.to_s
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.at("id").inner_text
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.at('title').inner_text
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.at("updated").inner_text)
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.at("author"))
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.search("category").map do |category_element|
47
- Category.new(category_element["term"], category_element["scheme"])
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.at("content/changes"))
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.at(
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["href"].split('/').last.split('.')[0..-2].join.to_i
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.at(
120
- "link[@rel='http://www.thoughtworks-studios.com/ns/mingle#version'][@type='application/vnd.mingle+xml']"
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.search("link").map do |category_element|
14
- Link.new(category_element["href"], category_element["rel"], category_element["type"], category_element["title"])
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