cherby 0.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.
@@ -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
+