cherby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ .ruby-version
2
+ .ruby-gemset
3
+ coverage/*
4
+ Gemfile.lock
5
+ doc/*
6
+ .yardoc/*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ --pattern "spec/**/*_spec.rb"
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+
@@ -0,0 +1,4 @@
1
+ --readme README.md
2
+ --markup markdown
3
+ lib/**/*.rb
4
+ -
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'bundler'
4
+
5
+ # Get all other dependencies from cherby.gemspec
6
+ gemspec
@@ -0,0 +1,83 @@
1
+ Cherby
2
+ ======
3
+
4
+ Cherby is a Ruby wrapper for the
5
+ [Cherwell Web Service](http://cherwellsupport.com/webhelp/cherwell/index.htm#1971.htm).
6
+
7
+ [Full documentation is on rdoc.info](http://rubydoc.info/github/a-e/cherby/master/frames).
8
+
9
+ [![Build Status](https://secure.travis-ci.org/a-e/cherby.png?branch=dev)](http://travis-ci.org/a-e/cherby)
10
+
11
+
12
+ Usage
13
+ -----
14
+
15
+ Connect to a Cherwell server by providing the URL of the web service:
16
+
17
+ url = "http://my.server/CherwellService/api.asmx"
18
+ cherwell = Cherby::Cherwell.new(url)
19
+
20
+ Login by providing username and password, either during instantiation, or later
21
+ when calling the `#login` method:
22
+
23
+ cherwell = Cherby::Cherwell.new(url, 'sisko', 'baseball')
24
+ cherwell.login
25
+ # => true
26
+
27
+ # or
28
+
29
+ cherwell = Cherby::Cherwell.new(url)
30
+ cherwell.login('sisko', 'baseball')
31
+ # => true
32
+
33
+ Fetch an Incident:
34
+
35
+ incident = cherwell.incident('12345')
36
+ # => #<Cherby::Incident:0x...>
37
+
38
+ View as a Hash:
39
+
40
+ incident.to_hash
41
+ # => {
42
+ # 'IncidentID' => '12345',
43
+ # 'Status' => 'Open',
44
+ # 'Priority' => '7',
45
+ # ...
46
+ # }
47
+
48
+ Make changes:
49
+
50
+ incident['Status'] = 'Closed'
51
+ incident['CloseDescription'] = 'Issue resolved'
52
+
53
+ Save back to Cherwell:
54
+
55
+ cherwell.save_incident(incident)
56
+
57
+
58
+ Copyright
59
+ ---------
60
+
61
+ The MIT License
62
+
63
+ Copyright (c) 2014 Eric Pierce
64
+
65
+ Permission is hereby granted, free of charge, to any person obtaining
66
+ a copy of this software and associated documentation files (the
67
+ "Software"), to deal in the Software without restriction, including
68
+ without limitation the rights to use, copy, modify, merge, publish,
69
+ distribute, sublicense, and/or sell copies of the Software, and to
70
+ permit persons to whom the Software is furnished to do so, subject to
71
+ the following conditions:
72
+
73
+ The above copyright notice and this permission notice shall be
74
+ included in all copies or substantial portions of the Software.
75
+
76
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
77
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
78
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
79
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
80
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
81
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
82
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
83
+
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ Bundler.setup
6
+
7
+ require 'rake'
8
+
9
+ Dir.glob('tasks/*.rake').each { |r| import r }
10
+
11
+ task :default => [:spec]
12
+
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "cherby"
3
+ s.version = "0.0.1"
4
+ s.summary = "Cherwell-Ruby bridge"
5
+ s.description = <<-EOS
6
+ Cherby is a Ruby wrapper for the Cherwell Web Service.
7
+ EOS
8
+ s.authors = ["Eric Pierce"]
9
+ s.email = "wapcaplet88@gmail.com"
10
+ s.homepage = "http://github.com/a-e/cherby"
11
+ s.platform = Gem::Platform::RUBY
12
+
13
+ s.add_dependency "httpclient"
14
+ s.add_dependency 'savon', '>= 2.3.0'
15
+ s.add_dependency 'yajl-ruby'
16
+ s.add_dependency 'nokogiri'
17
+ s.add_dependency 'mustache'
18
+
19
+ s.add_development_dependency "rake"
20
+ s.add_development_dependency "simplecov"
21
+ s.add_development_dependency "pry"
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency 'yard'
24
+ s.add_development_dependency 'redcarpet'
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.require_path = 'lib'
28
+ end
29
+
@@ -0,0 +1,11 @@
1
+ require 'cherby/business_object'
2
+ require 'cherby/client'
3
+ require 'cherby/incident'
4
+ require 'cherby/journal_note'
5
+ require 'cherby/task'
6
+ require 'cherby/cherwell'
7
+ require 'cherby/exceptions'
8
+
9
+ module Cherby
10
+ end
11
+
@@ -0,0 +1,159 @@
1
+ require 'mustache'
2
+ require 'nokogiri'
3
+
4
+ module Cherby
5
+ # Cherwell BusinessObject wrapper, with data represented as an XML DOM
6
+ class BusinessObject
7
+
8
+ # Override this with the value of the BusinessObject's 'Name' attribute
9
+ @object_name = ''
10
+ # Override this with the name of the Mustache XML template used to render
11
+ # your BusinessObject
12
+ @template = ''
13
+ # Fill this with default values for new instances of your BusinessObject
14
+ @default_values = {}
15
+
16
+ class << self
17
+ attr_accessor :object_name, :template, :default_values, :template_path
18
+ end
19
+
20
+ # Create a new BusinessObject subclass instance from the given hash of
21
+ # options.
22
+ # FIXME: Make this method accept CamelCase string field names instead of
23
+ # just :snake_case symbols, as part of a larger strategy to treat field names
24
+ # consistently throughout (using Cherwell's CamelCase strings everywhere)
25
+ def self.create(options={})
26
+ if self.template.empty?
27
+ # TODO: Exception subclass
28
+ raise RuntimeError, "No template defined for BusinessObject"
29
+ end
30
+ Mustache.template_path = File.join(File.dirname(__FILE__), 'templates')
31
+ xml = Mustache.render_file(
32
+ self.template, self.default_values.merge(options))
33
+ return self.new(xml)
34
+ end
35
+
36
+ # Instance methods
37
+
38
+ attr_reader :dom
39
+
40
+ # Create a new instance populated with the given XML string
41
+ def initialize(xml)
42
+ @dom = Nokogiri::XML(xml)
43
+ end
44
+
45
+ # Return the XML representation of this BusinessObject
46
+ def to_xml
47
+ return @dom.to_xml
48
+ end
49
+
50
+ # Return the node of the field with the given name.
51
+ def get_field_node(field_name)
52
+ selector = "BusinessObject > FieldList > Field[@Name=#{field_name}]"
53
+ return @dom.css(selector).first
54
+ end
55
+
56
+ # Return a hash of field names and values
57
+ def to_hash
58
+ result = {}
59
+ selector = "BusinessObject > FieldList > Field"
60
+ @dom.css(selector).each do |node|
61
+ result[node['Name']] = node.content
62
+ end
63
+ return result
64
+ end
65
+ alias :field_values :to_hash # For backwards compatibility
66
+
67
+
68
+ # Parse a Cherwell date/time string and return a DateTime object in UTC.
69
+ #
70
+ # This method mostly exists to work around the fact that Cherwell does
71
+ # not report a time zone offset in its datestamps. Since a BusinessObject
72
+ # may be initialized from a Jira entity (which *does* store time zone
73
+ # offset), any dt_string that includes a time zone offset at the end is
74
+ # correctly included in the result.
75
+ #
76
+ # @param [String] dt_string
77
+ # The date/time string to parse. May or may not include a trailing
78
+ # [+-]HH:MM or [+-]HHMM.
79
+ #
80
+ # @param [Integer] tz_offset
81
+ # Offset in hours (positive or negative) between UTC and the given
82
+ # `dt_string`. For example, Eastern Time is `-5`. This is ONLY used if
83
+ # `dt_string` does NOT include a trailing offset component.
84
+ #
85
+ def self.parse_datetime(dt_string, tz_offset=-5)
86
+ begin
87
+ result = DateTime.parse(dt_string)
88
+ rescue
89
+ raise ArgumentError, "Could not parse date/time '#{dt_string}'"
90
+ end
91
+ # If offset was part of the dt_string, use new_offset to get UTC
92
+ if dt_string =~ /[+-]\d\d:?\d\d$/
93
+ return result.new_offset(0)
94
+ # Otherwise, subtract the numeric offset to get UTC time
95
+ else
96
+ return result - Rational(tz_offset.to_i, 24)
97
+ end
98
+ end
99
+
100
+ # Return the last-modified date/time of this BusinessObject
101
+ # (LastModDateTime converted to DateTime)
102
+ def modified
103
+ last_mod = self['LastModDateTime']
104
+ if last_mod.nil?
105
+ raise RuntimeError, "BusinessObject is missing LastModDateTime field."
106
+ end
107
+ begin
108
+ return BusinessObject.parse_datetime(last_mod)
109
+ rescue(ArgumentError)
110
+ raise RuntimeError, "Cannot parse LastModDateTime: '#{last_mod}'"
111
+ end
112
+ end
113
+
114
+ # Return the last-modified time as a human-readable string
115
+ def mod_s
116
+ return modified.strftime('%Y-%m-%d %H:%M:%S')
117
+ end
118
+
119
+ # Return True if this BusinessObject was modified more recently than
120
+ # another BusinessObject.
121
+ def newer_than?(business_object)
122
+ return modified > business_object.modified
123
+ end
124
+
125
+ # Return the content in the field with the given name.
126
+ # TODO: Exception for unknown field name
127
+ def [](field_name)
128
+ if field = get_field_node(field_name)
129
+ return field.content
130
+ end
131
+ end
132
+
133
+ # Modify the content in the field with the given name.
134
+ # TODO: Exception for unknown field name
135
+ def []=(field_name, value)
136
+ if field = get_field_node(field_name)
137
+ field.content = value.to_s
138
+ end
139
+ end
140
+
141
+ # Copy designated fields from one BusinessObject to another.
142
+ #
143
+ # @example
144
+ # object_a.copy_fields_from(object_b, 'Status', 'Description')
145
+ # # object_a['Status'] = object_b['Status']
146
+ # # object_a['Description'] = object_b['Description']
147
+ #
148
+ # @param [BusinessObject] other_object
149
+ # The object to copy field values from
150
+ # @param [Array<String>] field_names
151
+ # Names of fields whose values you want to copy
152
+ #
153
+ def copy_fields_from(other_object, *field_names)
154
+ field_names.each do |field|
155
+ self[field] = other_object[field]
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,230 @@
1
+ require 'savon'
2
+ require 'nokogiri'
3
+ require 'cherby/client'
4
+ require 'cherby/incident'
5
+ require 'cherby/task'
6
+ require 'cherby/exceptions'
7
+
8
+ module Cherby
9
+ # Top-level Cherwell interface
10
+ class Cherwell
11
+ attr_reader :url, :username, :client
12
+
13
+ # Connect to a Cherwell server.
14
+ #
15
+ # @param [String] web_service_url
16
+ # Full URL to the Cherwell web service API (typically ending in
17
+ # `api.asmx`)
18
+ # @param [String] username
19
+ # Default Cherwell user ID to use
20
+ # @param [String] password
21
+ # Default Cherwell password to use
22
+ #
23
+ def initialize(web_service_url, username=nil, password=nil)
24
+ @url = web_service_url
25
+ @url.chop! if @url =~ /\/$/ # Remove any trailing slash
26
+ @username = username
27
+ @password = password
28
+ @client = Cherby::Client.new(@url)
29
+ end
30
+
31
+ # Login to Cherwell using the given credentials. Return true if
32
+ # login succeeded, or raise `LoginFailed` if login failed.
33
+ #
34
+ # @param [String] username
35
+ # User ID to login with. If omitted, the username that was passed to
36
+ # `Cherwell.new` is used.
37
+ # @param [String] password
38
+ # Password to login with. If omitted, the password that was passed to
39
+ # `Cherwell.new` is used.
40
+ #
41
+ # @return [Boolean]
42
+ # `true` if login was successful
43
+ #
44
+ # @raise [LoginFailed]
45
+ # If login failed for any reason
46
+ #
47
+ def login(username=nil, password=nil)
48
+ creds = {
49
+ :userId => username || @username,
50
+ :password => password || @password,
51
+ }
52
+ begin
53
+ response = @client.call(:login, :message => creds)
54
+ rescue => e
55
+ # This can happen if a bad URL is given
56
+ raise LoginFailed, e.message
57
+ else
58
+ if response.body[:login_response][:login_result] == true
59
+ # FIXME: Using the workaround described in this issue:
60
+ # https://github.com/savonrb/savon/issues/363
61
+ # because the version recommended in the documentation:
62
+ # auth_cookies = response.http.cookies
63
+ # does not work, giving:
64
+ # NoMethodError: undefined method `cookies' for #<HTTPI::Response:0x...>
65
+ @client.globals[:headers] = {"Cookie" => response.http.headers["Set-Cookie"]}
66
+ return true
67
+ # This can happen if invalid credentials are given
68
+ else
69
+ raise LoginFailed, "Cherwell returned false status"
70
+ end
71
+ end
72
+ end
73
+
74
+ # Log out of Cherwell.
75
+ #
76
+ # @return [Boolean]
77
+ # Logout response as reported by Cherwell.
78
+ #
79
+ def logout
80
+ return @client.logout
81
+ end
82
+
83
+ # Get the Cherwell incident with the given public ID, and return an
84
+ # Incident object.
85
+ #
86
+ # @return [Incident]
87
+ #
88
+ def incident(id)
89
+ incident_xml = get_object_xml('Incident', id)
90
+ return Incident.new(incident_xml.to_s)
91
+ end
92
+
93
+ # Get the Cherwell task with the given public ID, and return a Task
94
+ # object.
95
+ #
96
+ # @return [Task]
97
+ #
98
+ def task(id)
99
+ task_xml = get_object_xml('Task', id)
100
+ return Task.new(task_xml.to_s)
101
+ end
102
+
103
+ # Get a business object based on its public ID or RecID, and return the
104
+ # XML response.
105
+ #
106
+ # @example
107
+ # incident_xml = cherwell.get_object_xml(
108
+ # 'Incident', '12345')
109
+ #
110
+ # note_xml = cherwell.get_object_xml(
111
+ # 'JournalNote', '93bd7e3e067f1dafb454d14cb399dda1ef3f65d36d')
112
+ #
113
+ # @param [String] object_type
114
+ # What type of object to fetch, for example "Incident", "Customer",
115
+ # "Task", "JournalNote", "SLA" etc. May also be the `IDREF` of an
116
+ # object type. Cherwell's API knows this as `busObNameOrId`.
117
+ # @param [String] id
118
+ # The public ID or RecID of the object. If this is 32 characters or
119
+ # more, it's assumed to be a RecID. For incidents, the public ID is a
120
+ # numeric identifier like "50629", while the RecID is a long
121
+ # hexadecimal string like "93bd7e3e067f1dafb454d14cb399dda1ef3f65d36d".
122
+ #
123
+ # This invokes `GetBusinessObject` or `GetBusinessObjectByPublicId`,
124
+ # depending on the length of `id`. The returned XML is the content of the
125
+ # `GetBusinessObjectResult` or `GetBusinessObjectByPublicIdResult`.
126
+ #
127
+ # @return [String]
128
+ # Raw XML response string.
129
+ #
130
+ def get_object_xml(object_type, id)
131
+ # Assemble the SOAP body
132
+ body = {:busObNameOrId => object_type}
133
+
134
+ # If ID is really long, it's probably a RecID
135
+ if id.to_s.length >= 32
136
+ method = :get_business_object
137
+ body[:busObRecId] = id
138
+ # Otherwise, assume it's a public ID
139
+ else
140
+ method = :get_business_object_by_public_id
141
+ body[:busObPublicId] = id
142
+ end
143
+
144
+ begin
145
+ result = @client.call_wrap(method, body)
146
+ rescue Savon::Error => e
147
+ raise SoapError, e.message
148
+ else
149
+ return result
150
+ end
151
+ end
152
+
153
+
154
+ # Update a given Cherwell object by submitting its XML to the SOAP
155
+ # interface.
156
+ #
157
+ # @param [String] object_type
158
+ # The kind of object you're updating ('Incident', 'Task'), or the
159
+ # IDREF of the object type.
160
+ # @param [String] id
161
+ # The public ID of the object
162
+ # @param [String] xml
163
+ # The XML body containing all the updates you want to make
164
+ #
165
+ def update_object_xml(object_type, id, xml)
166
+ @client.update_business_object_by_public_id({
167
+ :busObNameOrId => object_type,
168
+ :busObPublicId => id,
169
+ :updateXml => xml
170
+ })
171
+ return last_error
172
+ end
173
+
174
+ # Save the given Cherwell incident
175
+ def save_incident(incident)
176
+ update_object_xml('Incident', incident.id, incident.to_xml)
177
+ end
178
+
179
+ # Save the given Cherwell task
180
+ def save_task(task)
181
+ update_object_xml('Task', task.id, task.to_xml)
182
+ end
183
+
184
+ # Create a new Cherwell incident with the given data. If creation
185
+ # succeeds, return the Incident instance; otherwise, return `nil`.
186
+ #
187
+ # @example
188
+ # create_incident(
189
+ # :service => 'Consulting Services',
190
+ # :sub_category => 'New/Modified Functionality',
191
+ # :priority => '4',
192
+ # )
193
+ #
194
+ # @param [Hash] data
195
+ # Incident fields to initialize. All required fields must be filled
196
+ # in, or creation will fail. At minimum this includes `:service`,
197
+ # `:sub_category`, and `:priority`.
198
+ #
199
+ # @return [Incident, nil]
200
+ # The created incident, or `nil` if creation failed.
201
+ #
202
+ def create_incident(data)
203
+ incident = Incident.create(data)
204
+ result = @client.create_business_object({
205
+ :busObNameOrId => 'Incident',
206
+ :creationXml => incident.to_xml
207
+ })
208
+
209
+ # Result contains the public ID of the new incident, or nil if the
210
+ # incident-creation failed.
211
+ if !result.nil?
212
+ incident['IncidentID'] = result
213
+ return incident
214
+ else
215
+ return nil
216
+ end
217
+ end
218
+
219
+ # Get the last error reported by Cherwell.
220
+ #
221
+ # @return [String, nil]
222
+ # Text of the last error that occurred, or `nil` if there was no error.
223
+ #
224
+ def last_error
225
+ return @client.get_last_error
226
+ end
227
+
228
+ end # class Cherwell
229
+ end # module Cherby
230
+