mingle_events 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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