mingle_events 0.0.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 (41) hide show
  1. data/Gemfile +4 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.textile +117 -0
  4. data/lib/mingle_events.rb +26 -0
  5. data/lib/mingle_events/feed.rb +5 -0
  6. data/lib/mingle_events/feed/author.rb +28 -0
  7. data/lib/mingle_events/feed/category.rb +75 -0
  8. data/lib/mingle_events/feed/element_support.rb +19 -0
  9. data/lib/mingle_events/feed/entry.rb +96 -0
  10. data/lib/mingle_events/feed/page.rb +61 -0
  11. data/lib/mingle_events/http_error.rb +26 -0
  12. data/lib/mingle_events/mingle_basic_auth_access.rb +71 -0
  13. data/lib/mingle_events/mingle_oauth_access.rb +40 -0
  14. data/lib/mingle_events/poller.rb +32 -0
  15. data/lib/mingle_events/processors.rb +10 -0
  16. data/lib/mingle_events/processors/author_filter.rb +62 -0
  17. data/lib/mingle_events/processors/card_data.rb +102 -0
  18. data/lib/mingle_events/processors/card_type_filter.rb +28 -0
  19. data/lib/mingle_events/processors/category_filter.rb +19 -0
  20. data/lib/mingle_events/processors/custom_property_filter.rb +30 -0
  21. data/lib/mingle_events/processors/http_post_publisher.rb +20 -0
  22. data/lib/mingle_events/processors/pipeline.rb +20 -0
  23. data/lib/mingle_events/processors/puts_publisher.rb +17 -0
  24. data/lib/mingle_events/project_custom_properties.rb +33 -0
  25. data/lib/mingle_events/project_event_fetcher.rb +96 -0
  26. data/test/mingle_events/feed/author_test.rb +39 -0
  27. data/test/mingle_events/feed/category_test.rb +20 -0
  28. data/test/mingle_events/feed/entry_test.rb +140 -0
  29. data/test/mingle_events/feed/page_test.rb +82 -0
  30. data/test/mingle_events/poller_test.rb +47 -0
  31. data/test/mingle_events/processors/author_filter_test.rb +80 -0
  32. data/test/mingle_events/processors/card_data_test.rb +210 -0
  33. data/test/mingle_events/processors/card_type_filter_test.rb +51 -0
  34. data/test/mingle_events/processors/category_filter_test.rb +27 -0
  35. data/test/mingle_events/processors/custom_property_filter_test.rb +51 -0
  36. data/test/mingle_events/processors/pipeline_test.rb +32 -0
  37. data/test/mingle_events/project_custom_properties_test.rb +39 -0
  38. data/test/mingle_events/project_event_fetcher_test.rb +122 -0
  39. data/test/test_helper.rb +163 -0
  40. data/test/web_hook_server/web_hook_server.rb +6 -0
  41. metadata +140 -0
@@ -0,0 +1,28 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ # Filters events by card types. As events do not contain the type
5
+ # of the card, this filter requires a lookup against Mingle to
6
+ # determine the type of the card that sourced the event. In the case
7
+ # of the card's being deleted in the interim between the actual event
8
+ # and this filtering, the event will be filtered as there is no means
9
+ # to determine its type. Therefore, it's recommended to also
10
+ # subscribe a 'CardDeleted' processor to the same project.
11
+ class CardTypeFilter
12
+
13
+ def initialize(card_types, card_data)
14
+ @card_types = card_types
15
+ @card_data = card_data
16
+ end
17
+
18
+ def process_events(events)
19
+ events.select do |event|
20
+ event.card? &&
21
+ @card_data.for_card_event(event) &&
22
+ @card_types.include?(@card_data.for_card_event(event)[:card_type_name])
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ # Removes events from the stream that do not match all of the specified categories
5
+ class CategoryFilter
6
+
7
+ def initialize(categories)
8
+ @categories = categories
9
+ end
10
+
11
+ def process_events(events)
12
+ events.select do |event|
13
+ @categories.all?{|c| event.categories.include?(c)}
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ # Filters events by a single custom property value.
5
+ # As events will not necessarily contain this data,
6
+ # this filter requires a lookup against Mingle to
7
+ # determine the type of the card that sourced the event. In the case
8
+ # of the card's being deleted in the interim between the actual event
9
+ # and this filtering, the event will be filtered as there is no means
10
+ # to determine its type. Therefore, it's recommended to also
11
+ # subscribe a 'CardDeleted' processor to the same project.
12
+ class CustomPropertyFilter
13
+
14
+ def initialize(property_name, property_value, card_data)
15
+ @property_name = property_name
16
+ @property_value = property_value
17
+ @card_data = card_data
18
+ end
19
+
20
+ def process_events(events)
21
+ events.select do |event|
22
+ event.card? &&
23
+ @card_data.for_card_event(event) &&
24
+ @property_value == @card_data.for_card_event(event)[:custom_properties][@property_name]
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ class HttpPostPublisher
5
+
6
+ def initialize(url)
7
+ @url = url
8
+ end
9
+
10
+ def process_events(events)
11
+ events.map{|e| process_event(e)}
12
+ end
13
+
14
+ def process_event(event)
15
+ Net::HTTP.post_form(URI.parse(@url), {'event' => event.raw_xml}).body
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ # Manages the passing of a stream of events through a sequence of processors
5
+ class Pipeline
6
+
7
+ def initialize(processors)
8
+ @processors = processors
9
+ end
10
+
11
+ def process_events(events)
12
+ processed_events = events
13
+ @processors.each do |processor|
14
+ processed_events = processor.process_events(processed_events)
15
+ end
16
+ processed_events
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module MingleEvents
2
+ module Processors
3
+
4
+ # Writes each event in stream to stdout, mostly for demonstration purposes
5
+ class PutsPublisher
6
+
7
+ def process_events(events)
8
+ events.map{|e| process_event(e)}
9
+ end
10
+
11
+ def process_event(event)
12
+ puts "Processing event #{event}"
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ module MingleEvents
2
+
3
+ class ProjectCustomProperties
4
+
5
+ def initialize(mingle_access, project_identifier)
6
+ @mingle_access = mingle_access
7
+ @project_identifier = project_identifier
8
+ end
9
+
10
+ def property_name_for_column(column_name)
11
+ property_names_by_column_name[column_name]
12
+ end
13
+
14
+ private
15
+
16
+ def property_names_by_column_name
17
+ @property_names_by_column_name ||= lookup_property_names_by_column_name
18
+ end
19
+
20
+ def lookup_property_names_by_column_name
21
+ as_document.search('/property_definitions/property_definition').inject({}) do |mapping, element|
22
+ mapping[element.at('column_name').inner_text] = element.at('name').inner_text
23
+ mapping
24
+ end
25
+ end
26
+
27
+ def as_document
28
+ @as_document ||= Nokogiri::XML(@mingle_access.fetch_page("/api/v2/projects/#{@project_identifier}/property_definitions.xml"))
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,96 @@
1
+ module MingleEvents
2
+
3
+ # fetch all unseen events and write them to disk for future processing
4
+ class ProjectEventFetcher
5
+
6
+ def initialize(project_identifier, mingle_access, state_dir=nil)
7
+ @project_identifier = project_identifier
8
+ @mingle_access = mingle_access
9
+ base_uri = URI.parse(mingle_access.base_url)
10
+ @state_dir = File.expand_path(state_dir || File.join('~', '.mingle_events', base_uri.host, base_uri.port.to_s, project_identifier, 'fetched_events'))
11
+ end
12
+
13
+ def reset
14
+ FileUtils.rm_rf(@state_dir)
15
+ end
16
+
17
+ def fetch_latest
18
+ page = Feed::Page.new("/api/v2/projects/#{@project_identifier}/feeds/events.xml", @mingle_access)
19
+ most_recent_new_entry = page.entries.first
20
+
21
+ last_fetched_entry = load_last_fetched_entry
22
+ fetched_previously_seen_event = false
23
+ next_entry = nil
24
+ while !fetched_previously_seen_event && page
25
+ page.entries.each do |entry|
26
+
27
+ if last_fetched_entry && entry.entry_id == last_fetched_entry.entry_id
28
+ fetched_previously_seen_event = true
29
+ break
30
+ end
31
+
32
+ write_entry_to_disk(entry, next_entry)
33
+ next_entry = entry
34
+
35
+ end
36
+ page = page.next
37
+ end
38
+
39
+ update_current_state(most_recent_new_entry)
40
+
41
+ file_for_entry(next_entry)
42
+ end
43
+
44
+ def file_for_entry(entry)
45
+ return nil if entry.nil?
46
+
47
+ entry_id_as_uri = URI.parse(entry.entry_id)
48
+ relative_path_parts = entry_id_as_uri.path.split('/')
49
+ entry_id_int = relative_path_parts.last
50
+ insertions = ["#{entry_id_int.to_i/16384}", "#{entry_id_int.to_i%16384}"]
51
+ relative_path_parts = relative_path_parts[0..-2] + insertions + ["#{entry_id_int}.yml"]
52
+ File.join(@state_dir, *relative_path_parts)
53
+ end
54
+
55
+ def current_state_file
56
+ File.expand_path(File.join(@state_dir, 'current_state.yml'))
57
+ end
58
+
59
+ private
60
+
61
+ def load_last_fetched_entry
62
+ current_state = if File.exist?(current_state_file)
63
+ YAML.load(File.new(current_state_file))
64
+ else
65
+ {:last_fetched_entry_info_file => nil}
66
+ end
67
+ last_fetched_entry = if current_state[:last_fetched_entry_info_file]
68
+ last_fetched_entry_info = YAML.load(File.new(current_state[:last_fetched_entry_info_file]))
69
+ Feed::Entry.new(Nokogiri::XML(last_fetched_entry_info[:entry_xml]).at('/entry'))
70
+ else
71
+ nil
72
+ end
73
+ end
74
+
75
+ def update_current_state(most_recent_new_entry)
76
+ if most_recent_new_entry
77
+ File.open(current_state_file, 'w') do |out|
78
+ YAML.dump({:last_fetched_entry_info_file => file_for_entry(most_recent_new_entry)}, out)
79
+ end
80
+ end
81
+ end
82
+
83
+ def write_entry_to_disk(entry, next_entry)
84
+ file = file_for_entry(entry)
85
+ FileUtils.mkdir_p(File.dirname(file))
86
+ file_content = {
87
+ :entry_xml => entry.raw_xml,
88
+ :next_entry_file_path => file_for_entry(next_entry)
89
+ }
90
+ File.open(file, 'w') do |out|
91
+ YAML.dump(file_content, out)
92
+ end
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Feed
5
+
6
+ class AuthorTest < Test::Unit::TestCase
7
+
8
+ def test_parse_attributes
9
+ element = Nokogiri.XML(%{
10
+ <author xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
11
+ <name>Sammy Soso</name>
12
+ <email>sammy@example.com</email>
13
+ <uri>https://mingle.example.com/api/v2/users/233.xml</uri>
14
+ <mingle:icon>https://mingle.example.com/user/icon/233/profile.jpg</mingle:icon>
15
+ </author>})
16
+
17
+ author = Author.new(element)
18
+ assert_equal("Sammy Soso", author.name)
19
+ assert_equal("sammy@example.com", author.email)
20
+ assert_equal("https://mingle.example.com/api/v2/users/233.xml", author.uri)
21
+ assert_equal("https://mingle.example.com/user/icon/233/profile.jpg", author.icon_uri)
22
+ end
23
+
24
+ def test_parse_attributes_when_no_optional_fields
25
+ element = Nokogiri.XML(%{
26
+ <author xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
27
+ <name>Sammy Soso</name>
28
+ </author>})
29
+ author = Author.new(element)
30
+ assert_equal("Sammy Soso", author.name)
31
+ assert_nil(author.email)
32
+ assert_nil(author.uri)
33
+ assert_nil(author.icon_uri)
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Feed
5
+
6
+ class CategoryTest < Test::Unit::TestCase
7
+
8
+ def test_equality
9
+ assert_equal(Category::CARD, Category::CARD)
10
+ assert_equal(Category::CARD, Category.new('card', 'http://www.thoughtworks-studios.com/ns/mingle#categories'))
11
+ assert_not_equal(Category::CARD, Category::PAGE)
12
+ assert_not_equal(Category::CARD, nil)
13
+ assert_not_equal(Category::CARD, Object.new)
14
+ assert_equal(:foo, {Category::CARD => :foo}[Category.new('card', 'http://www.thoughtworks-studios.com/ns/mingle#categories')])
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,140 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+
3
+ module MingleEvents
4
+ module Feed
5
+
6
+ class EntryTest < Test::Unit::TestCase
7
+
8
+ def test_parse_basic_attributes
9
+ element_xml_text = %{
10
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
11
+ <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
12
+ <title>Page Special:HeaderActions changed</title>
13
+ <updated>2011-02-03T08:12:42Z</updated>
14
+ <author>
15
+ <name>Sammy Soso</name>
16
+ <email>sammy@example.com</email>
17
+ <uri>https://mingle.example.com/api/v2/users/233.xml</uri>
18
+ </author>
19
+ </entry>}
20
+ element = Nokogiri::XML(element_xml_text)
21
+
22
+ entry = Entry.new(element)
23
+ # assert_equal(element_xml_text.inspect, entry.raw_xml.inspect)
24
+ assert_equal("https://mingle.example.com/projects/mingle/events/index/234443", entry.entry_id)
25
+ assert_equal("Page Special:HeaderActions changed", entry.title)
26
+ assert_equal("Thu Feb 03 08:12:42 UTC 2011", entry.updated.to_s)
27
+ assert_equal("Sammy Soso", entry.author.name)
28
+ end
29
+
30
+ def test_parse_categories
31
+ element_xml_text = %{
32
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
33
+ <category term="foo" scheme='http://tws.com/ns#mingle' />
34
+ <category term="bar" scheme="http://tws.com/ns#go" />
35
+ </entry>}
36
+ element = Nokogiri::XML(element_xml_text)
37
+
38
+ entry = Entry.new(element)
39
+ assert_equal(
40
+ [Category.new('foo', 'http://tws.com/ns#mingle'), Category.new('bar', 'http://tws.com/ns#go')],
41
+ entry.categories
42
+ )
43
+ end
44
+
45
+ def test_parse_card_number_and_version
46
+
47
+ # the links below contain intentionally nonsensical data so as to ensure
48
+ # that the card number is derived from a single, precise position
49
+
50
+ element_xml_text = %{
51
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
52
+ <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
53
+ <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"/>
54
+ <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)"/>
55
+ <link href="https://mingle.example.com/api/v2/projects/atlas/cards/106.xml" rel="http://www.thoughtworks-studios.com/ns/mingle#event-source" type="application/vnd.mingle+xml" title="bug #107"/>
56
+ <link href="https://mingle.example.com/projects/atlas/cards/108?version=17" rel="http://www.thoughtworks-studios.com/ns/mingle#version" type="text/html" title="bug #109 (v7)"/>
57
+ </entry>}
58
+ element = Nokogiri::XML(element_xml_text)
59
+
60
+ entry = Entry.new(element)
61
+ assert_equal(106, entry.card_number)
62
+ assert_equal(7, entry.version)
63
+ end
64
+
65
+ def test_card_number_and_version_throws_error_when_event_not_related_to_a_card
66
+ element_xml_text = %{
67
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
68
+ <category term="page" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
69
+ </entry>}
70
+ element = Nokogiri::XML(element_xml_text)
71
+
72
+ entry = Entry.new(element)
73
+
74
+ begin
75
+ entry.card_number
76
+ fail("Should not have been able to retrieve a card number for non card-related event!")
77
+ rescue Exception => e
78
+ assert_equal("You cannot get the card number for an event that is not sourced by a card!", e.message)
79
+ end
80
+
81
+ begin
82
+ entry.version
83
+ fail("Should not have been able to retrieve a card version for non card-related event!")
84
+ rescue Exception => e
85
+ assert_equal("You cannot get card version data for an event that is not sourced by a card!", e.message)
86
+ end
87
+
88
+ end
89
+
90
+ def test_parse_card_version_resource_uri
91
+
92
+ # the links below contain intentionally nonsensical data so as to ensure
93
+ # that the card number is derived from a single, precise position
94
+
95
+ element_xml_text = %{
96
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
97
+ <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
98
+ <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"/>
99
+ <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)"/>
100
+ <link href="https://mingle.example.com/api/v2/projects/atlas/cards/106.xml" rel="http://www.thoughtworks-studios.com/ns/mingle#event-source" type="application/vnd.mingle+xml" title="bug #107"/>
101
+ <link href="https://mingle.example.com/projects/atlas/cards/108?version=7" rel="http://www.thoughtworks-studios.com/ns/mingle#version" type="text/html" title="bug #109 (v7)"/>
102
+ </entry>}
103
+ element = Nokogiri::XML(element_xml_text)
104
+
105
+ entry = Entry.new(element)
106
+ assert_equal('https://mingle.example.com/api/v2/projects/atlas/cards/104.xml?version=7', entry.card_version_resource_uri)
107
+ end
108
+
109
+ def test_card_version_resource_uri_throws_error_when_not_card_event
110
+ element_xml_text = %{
111
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
112
+ <category term="page" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
113
+ </entry>}
114
+ element = Nokogiri::XML(element_xml_text)
115
+
116
+ entry = Entry.new(element)
117
+ begin
118
+ entry.card_version_resource_uri
119
+ fail("Should not have been able to retrieve a card version resource URI for non card-related event!")
120
+ rescue Exception => e
121
+ assert_equal("You cannot get card version data for an event that is not sourced by a card!", e.message)
122
+ end
123
+ end
124
+
125
+ def test_entry_id_aliased_as_event_id
126
+ element_xml_text = %{
127
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
128
+ <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
129
+ </entry>}
130
+ element = Nokogiri::XML(element_xml_text)
131
+
132
+ entry = Entry.new(element)
133
+ assert_equal('https://mingle.example.com/projects/mingle/events/index/234443', entry.event_id)
134
+ assert_equal(entry.entry_id, entry.event_id)
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+ end