cherby 0.0.3 → 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.
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "cherby"
3
- s.version = "0.0.3"
3
+ s.version = "0.0.4"
4
4
  s.summary = "Cherwell-Ruby bridge"
5
5
  s.description = <<-EOS
6
6
  Cherby is a Ruby wrapper for the Cherwell Web Service.
@@ -3,22 +3,27 @@ require 'nokogiri'
3
3
  module Cherby
4
4
  # Cherwell BusinessObject wrapper, with data represented as an XML DOM
5
5
  class BusinessObject
6
+ # Return the name of this class.
7
+ #
8
+ # @return [String]
9
+ #
10
+ def self.object_type
11
+ return self.name.split('::').last
12
+ end
6
13
 
7
- # Override this with the value of the BusinessObject's 'Name' attribute
8
- @object_name = ''
9
- # Fill this with default values for new instances of your BusinessObject
10
- @default_values = {}
11
-
12
- class << self
13
- attr_accessor :object_name, :default_values
14
+ # Return the name of this instance's class.
15
+ #
16
+ # @return [String]
17
+ #
18
+ def object_type
19
+ return self.class.object_type
14
20
  end
15
21
 
16
22
  # Create a new BusinessObject subclass instance from the given hash of
17
23
  # 'FieldName' => 'Field Value'.
18
24
  def self.create(fields={})
19
- type_name = self.object_name
20
25
  builder = Nokogiri::XML::Builder.new {
21
- BusinessObject_('Name' => type_name, 'RecID' => 'TODO') {
26
+ BusinessObject_('Name' => self.object_type, 'RecID' => 'TODO') {
22
27
  FieldList_ {
23
28
  fields.each { |name, value|
24
29
  Field_(value, 'Name' => name)
@@ -35,6 +40,24 @@ module Cherby
35
40
  attr_reader :dom
36
41
 
37
42
  # Create a new BusinessObject instance from the given XML string.
43
+ #
44
+ # @param [String] xml
45
+ # XML for the BusinessObject to create. The XML must contain a
46
+ # `BusinessObject` element, with a `FieldList` element within it,
47
+ # containing zero or more `Field` elements, each having a `Name`
48
+ # attribute, and a string value. For example:
49
+ #
50
+ # <BusinessObject Name="Customer">
51
+ # <FieldList>
52
+ # <Field Name="FirstName">Steven</Field>
53
+ # <Field Name="LastName">Moffat</Field>
54
+ # [...]
55
+ # </FieldList>
56
+ # </BusinessObject>
57
+ #
58
+ # @raise [Cherby::BadFormat]
59
+ # If the XML DOM's structure is incorrect
60
+ #
38
61
  def initialize(xml)
39
62
  @dom = Nokogiri::XML(xml)
40
63
  check_dom_format!
@@ -57,7 +80,11 @@ module Cherby
57
80
  return true
58
81
  end
59
82
 
60
- # Return the XML representation of this BusinessObject
83
+ # Return the XML representation of this BusinessObject.
84
+ #
85
+ # @return [String]
86
+ # The BusinessObject's XML representation
87
+ #
61
88
  def to_xml
62
89
  return @dom.to_xml
63
90
  end
@@ -71,6 +98,9 @@ module Cherby
71
98
  # The node for the `Field` element, or `nil` if no Field
72
99
  # with the given `Name` exists.
73
100
  #
101
+ # @raise [Cherby::BadFormat]
102
+ # If the XML DOM's structure is incorrect
103
+ #
74
104
  def get_field_node(field_name)
75
105
  check_dom_format!
76
106
  selector = "BusinessObject > FieldList > Field[@Name=#{field_name}]"
@@ -119,12 +149,12 @@ module Cherby
119
149
  # The date/time string to parse. May or may not include a trailing
120
150
  # [+-]HH:MM or [+-]HHMM.
121
151
  #
122
- # @param [Integer] tz_offset
152
+ # @param [Integer] default_tz_offset
123
153
  # Offset in hours (positive or negative) between UTC and the given
124
- # `dt_string`. For example, Eastern Time is `-5`. This is ONLY used if
154
+ # `dt_string`. For example, US Eastern Time is `-5`. This is ONLY used if
125
155
  # `dt_string` does NOT include a trailing offset component.
126
156
  #
127
- def self.parse_datetime(dt_string, tz_offset=-5)
157
+ def self.parse_datetime(dt_string, default_tz_offset=0)
128
158
  begin
129
159
  result = DateTime.parse(dt_string)
130
160
  rescue
@@ -133,27 +163,37 @@ module Cherby
133
163
  # If offset was part of the dt_string, use new_offset to get UTC
134
164
  if dt_string =~ /[+-]\d\d:?\d\d$/
135
165
  return result.new_offset(0)
136
- # Otherwise, subtract the numeric offset to get UTC time
166
+ # Otherwise, subtract the default offset to get UTC time
137
167
  else
138
- return result - Rational(tz_offset.to_i, 24)
168
+ return result - Rational(default_tz_offset.to_i, 24)
139
169
  end
140
170
  end
141
171
 
142
172
  # Return the last-modified date/time of this BusinessObject
143
- # (LastModDateTime converted to DateTime)
144
- def modified
173
+ # (LastModDateTime converted to DateTime).
174
+ #
175
+ # @param [Integer] default_tz_offset
176
+ # Offset in hours (positive or negative) between UTC and the given
177
+ # `dt_string`. For example, US Eastern Time is `-5`. This is ONLY used if
178
+ # `dt_string` does NOT include a trailing offset component.
179
+ #
180
+ # @raise [Cherby::MissingData, Cherby::BadFormat]
181
+ # If `LastModDateTime` is missing or cannot be parsed, respectively
182
+ #
183
+ def modified(default_tz_offset=0)
145
184
  last_mod = self['LastModDateTime']
146
185
  if last_mod.nil? || last_mod.empty?
147
- raise RuntimeError, "BusinessObject is missing LastModDateTime field."
186
+ raise Cherby::MissingData.new("BusinessObject is missing LastModDateTime field.")
148
187
  end
149
188
  begin
150
- return BusinessObject.parse_datetime(last_mod)
189
+ return BusinessObject.parse_datetime(last_mod, default_tz_offset)
151
190
  rescue(ArgumentError)
152
- raise RuntimeError, "Cannot parse LastModDateTime: '#{last_mod}'"
191
+ raise Cherby::BadFormat.new("Cannot parse LastModDateTime: '#{last_mod}'")
153
192
  end
154
193
  end
155
194
 
156
195
  # Return the last-modified time as a human-readable string
196
+ # TODO: Make this more error-tolerant with a default date?
157
197
  def mod_s
158
198
  return modified.strftime('%Y-%m-%d %H:%M:%S')
159
199
  end
@@ -1,7 +1,10 @@
1
1
  module Cherby
2
+ # Base class for all Cherby custom exceptions
2
3
  class CherbyError < RuntimeError; end
4
+
3
5
  class LoginFailed < CherbyError; end
4
6
  class NotFound < CherbyError; end
7
+ class MissingData < CherbyError; end
5
8
  class BadFormat < CherbyError; end
6
9
 
7
10
  class SoapError < CherbyError
@@ -6,18 +6,7 @@ require 'cherby/journal_note'
6
6
  module Cherby
7
7
  # Wrapper for Cherwell incident objects.
8
8
  class Incident < BusinessObject
9
- @object_name = 'Incident'
10
- # FIXME: Rename these and make use of them?
11
- @default_values = {
12
- :service => "Auto Generated",
13
- :service_group => "Auto Generated",
14
- :category => "Auto Generated",
15
- :sub_category => "JIRA",
16
- :impact => "Inconvenience",
17
- :urgency => "Medium",
18
- :priority => "3",
19
- }
20
-
9
+ # Return the Incident's public ID
21
10
  def id
22
11
  self['IncidentID']
23
12
  end
@@ -46,7 +35,7 @@ module Cherby
46
35
  self["SubcategoryNonHR"] = "JIRA"
47
36
  end
48
37
 
49
- # Return Task instances for all tasks associated with this Incident
38
+ # Return Task instances for all tasks associated with this Incident.
50
39
  #
51
40
  # @return [Array<Task>]
52
41
  #
@@ -56,7 +45,7 @@ module Cherby
56
45
  end
57
46
  end
58
47
 
59
- # Return all journal notes associated with this Incident.
48
+ # Return all JournalNotes associated with this Incident.
60
49
  #
61
50
  # @return [Array<JournalNote>]
62
51
  #
@@ -3,10 +3,6 @@ require 'cherby/business_object'
3
3
 
4
4
  module Cherby
5
5
  class JournalNote < BusinessObject
6
- @object_name = 'JournalNote'
7
- @default_values = {
8
- :details => "",
9
- }
10
6
  end
11
7
  end
12
8
 
@@ -2,16 +2,14 @@ require 'date'
2
2
  require 'cherby/business_object'
3
3
 
4
4
  module Cherby
5
+ # Wrapper for Cherwell task objects.
5
6
  class Task < BusinessObject
6
- @object_name = 'Task'
7
- @default_values = {
8
- :status => "New",
9
- }
10
-
7
+ # Return this task's public ID.
11
8
  def id
12
9
  self['TaskID']
13
10
  end
14
11
 
12
+ # Return true if this task exists in Cherwell.
15
13
  def exists?
16
14
  return !id.to_s.empty?
17
15
  end
@@ -1,25 +1,24 @@
1
1
  require_relative 'spec_helper'
2
2
  require 'cherby/business_object'
3
+ require 'cherby/exceptions'
3
4
 
4
5
  # Some BusinessObject subclasses to test with
5
- class MySubclass < Cherby::BusinessObject
6
- @object_name = 'MySubclass'
7
- @default_values = {}
8
- end
9
-
10
- class MySubclassNoTemplate < Cherby::BusinessObject
11
- @object_name = 'MySubclassNoTemplate'
12
- @default_values = {}
13
- end
14
-
15
- class MySubclassNoTemplateFile < Cherby::BusinessObject
16
- @object_name = 'MySubclassNoTemplateFile'
17
- @default_values = {}
18
- end
6
+ class MySubclass < Cherby::BusinessObject; end
19
7
 
20
8
  describe Cherby::BusinessObject do
21
9
  context "Class methods" do
10
+ describe "#object_type" do
11
+ it "returns the plain class name as a string" do
12
+ MySubclass.object_type.should == 'MySubclass'
13
+ end
14
+ end #object_type
15
+
22
16
  describe "#create" do
17
+ it "uses object_type as the BusinessObject `Name` attribute" do
18
+ obj = MySubclass.create({})
19
+ obj.dom.css("BusinessObject").first.attr('Name').should == 'MySubclass'
20
+ end
21
+
23
22
  it "sets options in the DOM" do
24
23
  obj = MySubclass.create({'First' => 'Eric', 'Last' => 'Idle'})
25
24
  first_name = obj.dom.css("BusinessObject[@Name=MySubclass] Field[@Name=First]").first
@@ -58,6 +57,13 @@ describe Cherby::BusinessObject do
58
57
 
59
58
 
60
59
  context "Instance methods" do
60
+ describe "#object_type" do
61
+ it "returns the plain class name as a string" do
62
+ obj = MySubclass.create({})
63
+ obj.object_type.should == 'MySubclass'
64
+ end
65
+ end #object_type
66
+
61
67
  describe "#initialize" do
62
68
  it "accepts an XML string" do
63
69
  xml = %Q{
@@ -75,7 +81,7 @@ describe Cherby::BusinessObject do
75
81
  last_name.content.should == "Idle"
76
82
  end
77
83
 
78
- it "raises BadFormat if XML is missing FieldList" do
84
+ it "raises Cherby::BadFormat if XML is missing FieldList" do
79
85
  xml = %Q{
80
86
  <BusinessObject Name="MySubclass">
81
87
  <Whatever/>
@@ -87,7 +93,7 @@ describe Cherby::BusinessObject do
87
93
  Cherby::BadFormat, /missing 'BusinessObject > FieldList'/)
88
94
  end
89
95
 
90
- it "raises BadFormat if XML is missing BusinessObject" do
96
+ it "raises Cherby::BadFormat if XML is missing BusinessObject" do
91
97
  xml = %Q{
92
98
  <Whatever Name="MySubclass"/>
93
99
  }
@@ -132,7 +138,7 @@ describe Cherby::BusinessObject do
132
138
  end #[]=
133
139
 
134
140
  describe "#modified" do
135
- it "BusinessObject with a valid LastModDateTime value" do
141
+ it "returns the parsed LastModDateTime if it's a valid date/time string" do
136
142
  # Parse the string version of now in order to truncate extra precision
137
143
  # (otherwise the date won't compare equal later)
138
144
  now = DateTime.parse(DateTime.now.to_s)
@@ -141,14 +147,26 @@ describe Cherby::BusinessObject do
141
147
  obj.modified.should == now
142
148
  end
143
149
 
144
- it "BusinessObject with an invalid LastModDateTime value" do
150
+ it "adjusts by default_tz_offset if LastModDateTime does not include an offset" do
151
+ # Current time, without timezone offset
152
+ now_str = DateTime.now.strftime('%Y-%m-%dT%H:%M:%S')
153
+ now = DateTime.parse(now_str)
154
+
155
+ obj = MySubclass.create({'LastModDateTime' => now_str})
156
+ # Test a bunch of different timezone offsets
157
+ [-7, -5, -2, 0, 1, 3, 4].each do |offset|
158
+ obj.modified(offset).should == now - Rational(offset, 24)
159
+ end
160
+ end
161
+
162
+ it "raises Cherby::BadFormat if LastModDateTime value is invalid" do
145
163
  obj = MySubclass.create({'LastModDateTime' => 'bogus'})
146
164
  lambda do
147
165
  obj.modified
148
- end.should raise_error(RuntimeError, /Cannot parse LastModDateTime: 'bogus'/)
166
+ end.should raise_error(Cherby::BadFormat, /Cannot parse LastModDateTime: 'bogus'/)
149
167
  end
150
168
 
151
- it "BusinessObject without a LastModDateTime field" do
169
+ it "raises Cherby::MissingData if LastModDateTime is empty" do
152
170
  xml = %Q{
153
171
  <BusinessObject Name="MySubclass">
154
172
  <FieldList>
@@ -172,8 +172,44 @@ describe Cherby::Cherwell do
172
172
  result = @cherwell.get_object_xml(@name, @public_id)
173
173
  result.should == xml
174
174
  end
175
+
176
+ it "raises Cherby::SoapError if Savon::SOAPFault occurs" do
177
+ soap_fault = Savon::SOAPFault.new('fake-http', 'fake-nori')
178
+ @cherwell.client.stub(:call_wrap).and_raise(soap_fault)
179
+ lambda do
180
+ @cherwell.get_object_xml(@name, @public_id)
181
+ end.should raise_error { |ex|
182
+ ex.should be_a(Cherby::SoapError)
183
+ ex.message.should =~ /SOAPFault from method/
184
+ ex.http.should == 'fake-http'
185
+ }
186
+ end
175
187
  end #get_object_xml
176
188
 
189
+ describe "#get_business_object" do
190
+ before(:each) do
191
+ @name = 'Thing'
192
+ @rec_id = '12345678901234567890123456789012'
193
+ @public_id = '12345'
194
+ @business_object_xml = %Q{
195
+ <BusinessObject>
196
+ <FieldList>
197
+ <Field Name="Name">#{@name}</Field>
198
+ <Field Name="RecID">#{@rec_id}</Field>
199
+ </FieldList>
200
+ </BusinessObject>
201
+ }
202
+ @cherwell.stub(:get_object_xml).
203
+ with(@name, @public_id).
204
+ and_return(@business_object_xml)
205
+ end
206
+
207
+ it "returns a BusinessObject instance" do
208
+ result = @cherwell.get_business_object(@name, @public_id)
209
+ result.should be_a(Cherby::BusinessObject)
210
+ end
211
+ end
212
+
177
213
  describe "#update_object_xml" do
178
214
  before(:each) do
179
215
  @public_id = '80909'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cherby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-03-31 00:00:00.000000000 Z
12
+ date: 2014-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: httpclient