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