fiddler 0.0.1 → 1.0.1
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.
- data/README.md +70 -0
- data/lib/fiddler.rb +5 -1
- data/lib/fiddler/attachment.rb +142 -0
- data/lib/fiddler/configuration.rb +7 -2
- data/lib/fiddler/connection_manager.rb +62 -20
- data/lib/fiddler/errors.rb +1 -0
- data/lib/fiddler/extensions.rb +2 -0
- data/lib/fiddler/extensions/file.rb +5 -0
- data/lib/fiddler/extensions/hash.rb +15 -0
- data/lib/fiddler/formatters/base_formatter.rb +3 -0
- data/lib/fiddler/formatters/search_request_formatter.rb +2 -2
- data/lib/fiddler/history.rb +32 -0
- data/lib/fiddler/parsers.rb +3 -1
- data/lib/fiddler/parsers/attachment_parser.rb +54 -0
- data/lib/fiddler/parsers/base_parser.rb +41 -3
- data/lib/fiddler/parsers/history_parser.rb +72 -0
- data/lib/fiddler/parsers/ticket_parser.rb +52 -21
- data/lib/fiddler/ticket.rb +141 -43
- data/lib/fiddler/version.rb +1 -1
- data/spec/attachment_spec.rb +26 -0
- data/spec/cassettes/change-ownership-take.yml +98 -0
- data/spec/cassettes/get-tickets.yml +89087 -0
- data/spec/cassettes/reply-to-tickets.yml +232 -0
- data/spec/cassettes/root-request.yml +79 -0
- data/spec/cassettes/search-tickets.yml +38249 -0
- data/spec/cassettes/ticket-histories-count.yml +2195 -0
- data/spec/cassettes/ticket-histories.yml +954 -0
- data/spec/connection_manager_spec.rb +5 -1
- data/spec/formatters/search_request_formatter_spec.rb +13 -0
- data/spec/parsers/attachment_parser_spec.rb +29 -0
- data/spec/parsers/base_parser_spec.rb +16 -0
- data/spec/parsers/history_parser_spec.rb +32 -0
- data/spec/parsers/ticket_parser_spec.rb +5 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/ticket_spec.rb +188 -6
- metadata +80 -9
data/lib/fiddler/parsers.rb
CHANGED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Fiddler
|
2
|
+
module Parsers
|
3
|
+
class AttachmentParser < BaseParser
|
4
|
+
def self.parse_single(response)
|
5
|
+
response = check_response_code(response)
|
6
|
+
response = check_for_errors(response)
|
7
|
+
attachment_from_response(response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.parse_content(response)
|
11
|
+
response = check_response_code(response,false,false)
|
12
|
+
return response.join("\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def self.check_for_errors(response)
|
18
|
+
if response.length == 1
|
19
|
+
raise Fiddler::FiddlerError, response.first
|
20
|
+
end
|
21
|
+
response
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.attachment_from_response(response)
|
25
|
+
result = {}
|
26
|
+
collect_headers = false
|
27
|
+
headers = []
|
28
|
+
response.each do |line|
|
29
|
+
matches = /^(\S*?):\s(.*)/.match(line)
|
30
|
+
if(matches)
|
31
|
+
key = matches[1].underscore
|
32
|
+
result[key] = matches[2]
|
33
|
+
|
34
|
+
if key == "headers"
|
35
|
+
collect_headers = true
|
36
|
+
next
|
37
|
+
elsif key == "content"
|
38
|
+
break
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
spaced_content_matches = /^\s{9}(.*)$/.match(line)
|
43
|
+
if spaced_content_matches and collect_headers
|
44
|
+
headers << spaced_content_matches[1]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
result.delete("content")
|
48
|
+
headers.unshift(result["headers"])
|
49
|
+
result["headers"] = headers
|
50
|
+
attachment = Fiddler::Attachment.new(result)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -5,19 +5,57 @@ module Fiddler
|
|
5
5
|
SUCCESS_CODES = (200..299).to_a
|
6
6
|
ERROR_CODES = (400..499).to_a
|
7
7
|
|
8
|
-
def self.check_response_code(response)
|
9
|
-
|
8
|
+
def self.check_response_code(response, reject_blank_lines=true, raise_error_for_empty_response=true)
|
9
|
+
response = safe_encode(response)
|
10
|
+
lines = response.split("\n")
|
11
|
+
lines = lines.delete_if { |l| l.nil? or l == "" } if reject_blank_lines
|
10
12
|
if lines.count == 0
|
11
|
-
|
13
|
+
if raise_error_for_empty_response
|
14
|
+
raise RequestError, "Empty Response"
|
15
|
+
else
|
16
|
+
return []
|
17
|
+
end
|
12
18
|
else
|
13
19
|
status_line = lines.shift
|
14
20
|
version, status_code, status_text = status_line.split(/\s+/,2)
|
15
21
|
unless SUCCESS_CODES.include?(status_code.to_i)
|
16
22
|
raise RequestError, status_text
|
17
23
|
end
|
24
|
+
lines.shift unless reject_blank_lines
|
18
25
|
lines
|
19
26
|
end
|
20
27
|
end
|
28
|
+
|
29
|
+
def self.tokenize_response(response)
|
30
|
+
tokens = Array.new
|
31
|
+
current = Array.new
|
32
|
+
response.each do |item|
|
33
|
+
if(item == "--")
|
34
|
+
tokens << current
|
35
|
+
current = Array.new
|
36
|
+
next
|
37
|
+
end
|
38
|
+
current << item
|
39
|
+
end
|
40
|
+
tokens << current unless current.empty?
|
41
|
+
tokens
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def self.safe_encode(response)
|
47
|
+
require 'iconv' unless String.method_defined?(:encode)
|
48
|
+
if String.method_defined?(:encode)
|
49
|
+
response.encode!('UTF-8', 'UTF-8', :invalid => :replace)
|
50
|
+
else
|
51
|
+
begin
|
52
|
+
Iconv.iconv("UTF-8//IGNORE", "UTF-8", response).join("")
|
53
|
+
rescue Exception => e
|
54
|
+
raise IllegalCharacterError, "Attachment contains illegal characters"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
response
|
58
|
+
end
|
21
59
|
end
|
22
60
|
end
|
23
61
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Fiddler
|
2
|
+
module Parsers
|
3
|
+
class HistoryParser < BaseParser
|
4
|
+
def self.parse_single(response)
|
5
|
+
response = check_response_code(response)
|
6
|
+
response = check_for_errors(response)
|
7
|
+
history_from_response(response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.parse_multiple(response)
|
11
|
+
response = check_response_code(response)
|
12
|
+
response = check_for_errors(response)
|
13
|
+
token_responses = tokenize_response(response)
|
14
|
+
histories = Array.new
|
15
|
+
token_responses.each do |token_response|
|
16
|
+
histories << history_from_response(token_response)
|
17
|
+
end
|
18
|
+
histories
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def self.check_for_errors(response)
|
24
|
+
if response.length == 1
|
25
|
+
raise Fiddler::FiddlerError, response.first
|
26
|
+
else
|
27
|
+
response.shift
|
28
|
+
end
|
29
|
+
response
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.history_from_response(response)
|
33
|
+
result = {}
|
34
|
+
content_lines = []
|
35
|
+
result["attachment_ids"] = []
|
36
|
+
collect_content = false
|
37
|
+
collect_attachments = false
|
38
|
+
response.each do |line|
|
39
|
+
matches = /^(\S*?):\s(.*)/.match(line)
|
40
|
+
if(matches)
|
41
|
+
key = matches[1].underscore
|
42
|
+
result[key] = matches[2]
|
43
|
+
if key == "content"
|
44
|
+
collect_content = true
|
45
|
+
elsif key == "attachments"
|
46
|
+
collect_content = false
|
47
|
+
collect_attachments = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if collect_content
|
52
|
+
content_matches = /^\s{9}(.*)$/.match(line)
|
53
|
+
if(content_matches)
|
54
|
+
content_lines << content_matches[1]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if collect_attachments
|
59
|
+
attachment_matches = /^\s{13}(.*?):\s(.*)/.match(line)
|
60
|
+
if(attachment_matches)
|
61
|
+
result["attachment_ids"] << attachment_matches[1]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
result["attachment_ids"].count.times { content_lines.pop }
|
66
|
+
result["content"] += content_lines.join("\n")
|
67
|
+
result.delete("attachments")
|
68
|
+
history = Fiddler::History.new(result)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -10,12 +10,39 @@ module Fiddler
|
|
10
10
|
def self.parse_multiple(response)
|
11
11
|
response = check_response_code(response)
|
12
12
|
response = check_for_errors(response)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
if response.count == 0
|
14
|
+
[]
|
15
|
+
else
|
16
|
+
ticket_token_responses = tokenize_response(response)
|
17
|
+
tickets = Array.new
|
18
|
+
ticket_token_responses.each do |token_response|
|
19
|
+
tickets << ticket_from_response(token_response)
|
20
|
+
end
|
21
|
+
tickets
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse_reply_response(response)
|
26
|
+
response = check_response_code(response)
|
27
|
+
return !response.first.match(/^# Message recorded/).nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.parse_change_ownership_response(response)
|
31
|
+
response = check_response_code(response)
|
32
|
+
if response.first =~ /^# Owner changed from (\S+) to (\S+)/
|
33
|
+
return $2
|
34
|
+
else
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.parse_update_response(response, method)
|
40
|
+
response = check_response_code(response)
|
41
|
+
if method == :create
|
42
|
+
return response.first =~ /^# Ticket (\S+) created/ ? $1 : nil
|
43
|
+
elsif method == :update
|
44
|
+
return response.first =~ /^# Ticket (\S+) updated/ ? $1 : nil
|
17
45
|
end
|
18
|
-
tickets
|
19
46
|
end
|
20
47
|
|
21
48
|
protected
|
@@ -24,35 +51,39 @@ module Fiddler
|
|
24
51
|
message = response.first.strip
|
25
52
|
if message =~ /^#/
|
26
53
|
raise Fiddler::TicketNotFoundError, message
|
54
|
+
elsif message == "No matching results."
|
55
|
+
response = []
|
27
56
|
end
|
28
57
|
response
|
29
58
|
end
|
30
59
|
|
31
60
|
def self.ticket_from_response(response)
|
32
61
|
result = {}
|
62
|
+
prev_key = nil
|
33
63
|
response.each do |line|
|
34
|
-
matches = /^(
|
64
|
+
matches = /^(\S*?):\s(.*)/.match(line)
|
35
65
|
if(matches)
|
36
66
|
key = matches[1].underscore
|
67
|
+
prev_key = key
|
37
68
|
result[key] = matches[2]
|
69
|
+
else
|
70
|
+
whitespace_matches = /^\s{12}(.*)/.match(line)
|
71
|
+
if whitespace_matches and prev_key
|
72
|
+
values = result[prev_key]
|
73
|
+
values = values.split(",") unless values.is_a?(Array)
|
74
|
+
result[prev_key] = values.concat(whitespace_matches[1].split(",")).collect { |x| x.strip }
|
75
|
+
end
|
38
76
|
end
|
39
77
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
tokens = Array.new
|
45
|
-
current = Array.new
|
46
|
-
response.each do |item|
|
47
|
-
if(item == "--")
|
48
|
-
tokens << current
|
49
|
-
current = Array.new
|
50
|
-
next
|
51
|
-
end
|
52
|
-
current << item
|
78
|
+
begin
|
79
|
+
id = result['id'].scan(/^ticket\/(\d*)$/).first.first.to_i
|
80
|
+
rescue
|
81
|
+
raise RequestError, "Unexpected response for id : #{result['id']}"
|
53
82
|
end
|
54
|
-
|
83
|
+
ticket = Fiddler::Ticket.new(result)
|
84
|
+
ticket.id = id
|
85
|
+
return ticket
|
55
86
|
end
|
56
87
|
end
|
57
88
|
end
|
58
|
-
end
|
89
|
+
end
|
data/lib/fiddler/ticket.rb
CHANGED
@@ -1,42 +1,147 @@
|
|
1
|
+
require 'active_attr'
|
1
2
|
module Fiddler
|
2
3
|
class Ticket
|
4
|
+
include ActiveAttr::Model
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
+
attribute :id, :default => 'ticket/new'
|
7
|
+
attribute :queue
|
8
|
+
attribute :owner
|
9
|
+
attribute :creator
|
10
|
+
attribute :subject
|
11
|
+
attribute :status
|
12
|
+
attribute :priority
|
13
|
+
attribute :initial_priority
|
14
|
+
attribute :final_priority
|
15
|
+
attribute :requestors
|
16
|
+
attribute :cc
|
17
|
+
attribute :admin_cc
|
18
|
+
attribute :created
|
19
|
+
attribute :starts
|
20
|
+
attribute :started
|
21
|
+
attribute :due
|
22
|
+
attribute :resolved
|
23
|
+
attribute :told
|
24
|
+
attribute :last_updated
|
25
|
+
attribute :time_estimated
|
26
|
+
attribute :time_worked
|
27
|
+
attribute :time_left
|
28
|
+
attribute :text
|
6
29
|
|
7
|
-
|
8
|
-
|
9
|
-
|
30
|
+
validates_presence_of :id, :queue, :subject
|
31
|
+
|
32
|
+
def histories
|
33
|
+
if @histories == nil
|
34
|
+
url = "ticket/#{id}/history"
|
35
|
+
response = Fiddler::ConnectionManager.get(url, {:format => "l"})
|
36
|
+
@histories = Fiddler::Parsers::HistoryParser.parse_multiple(response)
|
37
|
+
end
|
38
|
+
@histories
|
39
|
+
end
|
40
|
+
|
41
|
+
# make the setting and getting of requestors easier
|
42
|
+
def requestor_array
|
43
|
+
self.requestors.split(",").collect { |x| x.strip }
|
44
|
+
end
|
45
|
+
|
46
|
+
def requestor_array=(requestor_array)
|
47
|
+
if requestor_array.is_a?(Array)
|
48
|
+
self.requestors = requestor_array.join(", ")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add a comment to a ticket
|
53
|
+
# Example:
|
54
|
+
# tix = Ticket.get(1000)
|
55
|
+
# tix.comment("This is a comment", :time_worked => 45, :cc => 'someone@example.com')
|
56
|
+
#
|
57
|
+
# Attachments
|
58
|
+
# tix.comment("This is a comment", :attachments => "/tmp/filename.txt")
|
59
|
+
#
|
60
|
+
# Attachment as a file descriptor
|
61
|
+
# tix.correspond("This is a comment", :attachments => File.new("/tmp/filename.txt"))
|
10
62
|
#
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
63
|
+
# Attachment as a ActionDispatch::Http::UploadedFile instance
|
64
|
+
# tix.comment("This is a comment", :attachments => [params[:attachment_1], params[:attachment_2]])
|
65
|
+
#
|
66
|
+
def comment(comment, opt = {})
|
67
|
+
reply('Comment', comment, opt)
|
68
|
+
end
|
69
|
+
|
70
|
+
def correspond(comment, opt = {})
|
71
|
+
reply('Correspond', comment, opt)
|
72
|
+
end
|
73
|
+
|
74
|
+
def steal
|
75
|
+
change_ownership "Steal"
|
76
|
+
end
|
77
|
+
|
78
|
+
def take
|
79
|
+
change_ownership "Take"
|
80
|
+
end
|
81
|
+
|
82
|
+
def untake
|
83
|
+
change_ownership "Untake"
|
84
|
+
end
|
85
|
+
|
86
|
+
def save
|
87
|
+
id == "ticket/new" ? create : update
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
def reply(method, comment, opt)
|
93
|
+
# make sure the options are only the valid ones.
|
94
|
+
valid_options = [:cc, :bcc, :time_worked, :attachments, :status]
|
95
|
+
opt.delete_if { |key,value| !valid_options.include?(key) }
|
96
|
+
|
97
|
+
payload = { :text => comment, :action => method}.merge(opt)
|
98
|
+
payload.delete(:attachments)
|
99
|
+
|
100
|
+
attachments = Fiddler::AttachmentCollection.fill(opt[:attachments])
|
101
|
+
payload.merge!(:attachment => attachments.map(&:name).join(",")) unless attachments.empty?
|
102
|
+
|
103
|
+
response = Fiddler::ConnectionManager.post_content("ticket/#{id}/comment", { :content => payload.to_content_format }.merge(attachments.to_payload) )
|
104
|
+
|
105
|
+
result = Fiddler::Parsers::TicketParser.parse_reply_response(response)
|
106
|
+
|
107
|
+
# update the ticket parameters if the action was successful
|
108
|
+
if result and opt.has_key?(:status)
|
109
|
+
self.status = opt[:status]
|
110
|
+
result
|
15
111
|
else
|
16
|
-
|
112
|
+
result
|
17
113
|
end
|
18
|
-
@attributes.update(:id => 'ticket/new')
|
19
|
-
@saved = false
|
20
|
-
@histories = []
|
21
|
-
@new_record = true
|
22
|
-
add_methods!
|
23
114
|
end
|
24
115
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
116
|
+
def change_ownership(method)
|
117
|
+
payload = { "Action" => method }
|
118
|
+
response = Fiddler::ConnectionManager.post("ticket/#{id}/take", :content => payload.to_content_format)
|
119
|
+
new_owner = Fiddler::Parsers::TicketParser.parse_change_ownership_response(response)
|
120
|
+
if new_owner
|
121
|
+
self.owner = new_owner
|
122
|
+
return self
|
123
|
+
else
|
124
|
+
return nil
|
33
125
|
end
|
34
126
|
end
|
35
127
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
128
|
+
def create
|
129
|
+
return false unless valid?
|
130
|
+
response = Fiddler::ConnectionManager.post("ticket/new", :content => @attributes.to_content_format)
|
131
|
+
id = Fiddler::Parsers::TicketParser.parse_update_response(response, :create)
|
132
|
+
return false unless id
|
133
|
+
self.id = id.to_i
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
def update
|
138
|
+
return false unless valid?
|
139
|
+
payload = @attributes.clone
|
140
|
+
payload.delete("text")
|
141
|
+
payload.delete("id")
|
142
|
+
response = Fiddler::ConnectionManager.post("ticket/#{id}/edit", :content => payload.to_content_format)
|
143
|
+
id = Fiddler::Parsers::TicketParser.parse_update_response(response, :update)
|
144
|
+
return !id.nil?
|
40
145
|
end
|
41
146
|
|
42
147
|
# Class methods
|
@@ -49,6 +154,7 @@ module Fiddler
|
|
49
154
|
url = "ticket/#{id}"
|
50
155
|
response = Fiddler::ConnectionManager.get(url)
|
51
156
|
ticket = Fiddler::Parsers::TicketParser.parse_single(response)
|
157
|
+
ticket
|
52
158
|
end
|
53
159
|
|
54
160
|
# Search the tickets with the given conditions
|
@@ -56,9 +162,14 @@ module Fiddler
|
|
56
162
|
# @params [Hash] of conditions
|
57
163
|
# @returns [Array<Ticket>] of the tickets matching the criteria
|
58
164
|
def all(conditions={})
|
165
|
+
tickets = []
|
59
166
|
url = "search/ticket"
|
60
|
-
|
61
|
-
|
167
|
+
request_hash = Fiddler::Formatters::SearchRequestFormatter.format(conditions)
|
168
|
+
unless request_hash.empty?
|
169
|
+
response = Fiddler::ConnectionManager.get(url,request_hash)
|
170
|
+
tickets = Fiddler::Parsers::TicketParser.parse_multiple(response)
|
171
|
+
end
|
172
|
+
tickets
|
62
173
|
end
|
63
174
|
|
64
175
|
# Creates a new ticket with the given options, it will not save the ticket
|
@@ -67,19 +178,6 @@ module Fiddler
|
|
67
178
|
# @returns [Ticket] returns new ticket object
|
68
179
|
def create(options)
|
69
180
|
end
|
70
|
-
|
71
|
-
# Saves the changes to the ticket object
|
72
|
-
#
|
73
|
-
# @returns [boolean] save succcessful or not
|
74
|
-
def save
|
75
|
-
end
|
76
|
-
|
77
|
-
# Find the tickets with the given options
|
78
|
-
#
|
79
|
-
# @params [Hash] of options
|
80
|
-
# @returns [Array<Ticket>] of ticket matching the criteria
|
81
|
-
def find(options={})
|
82
|
-
end
|
83
181
|
end
|
84
182
|
end
|
85
|
-
end
|
183
|
+
end
|