allscripts_unity_client 1.0.3

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.
Files changed (67) hide show
  1. data/.gitignore +20 -0
  2. data/.travis.yml +3 -0
  3. data/Gemfile +2 -0
  4. data/LICENSE +22 -0
  5. data/README.md +180 -0
  6. data/Rakefile +7 -0
  7. data/allscripts_unity_client.gemspec +39 -0
  8. data/lib/allscripts_unity_client.rb +43 -0
  9. data/lib/allscripts_unity_client/client.rb +594 -0
  10. data/lib/allscripts_unity_client/client_driver.rb +95 -0
  11. data/lib/allscripts_unity_client/json_client_driver.rb +110 -0
  12. data/lib/allscripts_unity_client/json_unity_request.rb +33 -0
  13. data/lib/allscripts_unity_client/json_unity_response.rb +27 -0
  14. data/lib/allscripts_unity_client/soap_client_driver.rb +128 -0
  15. data/lib/allscripts_unity_client/timezone.rb +99 -0
  16. data/lib/allscripts_unity_client/unity_request.rb +63 -0
  17. data/lib/allscripts_unity_client/unity_response.rb +110 -0
  18. data/lib/allscripts_unity_client/utilities.rb +66 -0
  19. data/lib/allscripts_unity_client/version.rb +3 -0
  20. data/spec/allscripts_unity_client_spec.rb +57 -0
  21. data/spec/client_driver_spec.rb +71 -0
  22. data/spec/client_spec.rb +406 -0
  23. data/spec/factories/allscripts_unity_client_parameters_factory.rb +13 -0
  24. data/spec/factories/client_driver_factory.rb +14 -0
  25. data/spec/factories/client_factory.rb +7 -0
  26. data/spec/factories/json_client_driver_factory.rb +3 -0
  27. data/spec/factories/json_unity_request_factory.rb +3 -0
  28. data/spec/factories/json_unity_response_factory.rb +3 -0
  29. data/spec/factories/magic_request_factory.rb +33 -0
  30. data/spec/factories/soap_client_driver_factory.rb +3 -0
  31. data/spec/factories/timezone_factory.rb +7 -0
  32. data/spec/factories/unity_request_factory.rb +10 -0
  33. data/spec/factories/unity_response_factory.rb +8 -0
  34. data/spec/fixtures/attributes_hash.yml +15 -0
  35. data/spec/fixtures/date_hash.yml +8 -0
  36. data/spec/fixtures/date_string_hash.yml +8 -0
  37. data/spec/fixtures/error.json +3 -0
  38. data/spec/fixtures/get_providers.json +69 -0
  39. data/spec/fixtures/get_providers.xml +119 -0
  40. data/spec/fixtures/get_providers_json.yml +65 -0
  41. data/spec/fixtures/get_providers_xml.yml +270 -0
  42. data/spec/fixtures/get_security_token.json +1 -0
  43. data/spec/fixtures/get_security_token.xml +7 -0
  44. data/spec/fixtures/get_server_info.json +10 -0
  45. data/spec/fixtures/get_server_info.xml +40 -0
  46. data/spec/fixtures/get_server_info_json.yml +8 -0
  47. data/spec/fixtures/get_server_info_xml.yml +55 -0
  48. data/spec/fixtures/no_attributes_hash.yml +7 -0
  49. data/spec/fixtures/retire_security_token.json +1 -0
  50. data/spec/fixtures/retire_security_token.xml +5 -0
  51. data/spec/fixtures/soap_fault.xml +13 -0
  52. data/spec/fixtures/string_keyed_hash.yml +8 -0
  53. data/spec/fixtures/symbol_keyed_hash.yml +8 -0
  54. data/spec/json_client_driver_spec.rb +209 -0
  55. data/spec/json_unity_request_spec.rb +37 -0
  56. data/spec/json_unity_response_spec.rb +44 -0
  57. data/spec/soap_client_driver_spec.rb +201 -0
  58. data/spec/spec_helper.rb +44 -0
  59. data/spec/support/fixture_loader.rb +22 -0
  60. data/spec/support/shared_examples_for_client_driver.rb +139 -0
  61. data/spec/support/shared_examples_for_unity_request.rb +94 -0
  62. data/spec/support/shared_examples_for_unity_response.rb +26 -0
  63. data/spec/timezone_spec.rb +161 -0
  64. data/spec/unity_request_spec.rb +37 -0
  65. data/spec/unity_response_spec.rb +36 -0
  66. data/spec/utilities_spec.rb +69 -0
  67. metadata +323 -0
@@ -0,0 +1,95 @@
1
+ require 'logger'
2
+
3
+ module AllscriptsUnityClient
4
+ class ClientDriver
5
+ LOG_FILE = "logs/unity_client.log"
6
+
7
+ attr_accessor :username, :password, :appname, :base_unity_url, :proxy, :security_token, :timezone, :logger, :log
8
+
9
+ def initialize(base_unity_url, username, password, appname, proxy = nil, timezone = nil, logger = nil, log = true)
10
+ raise ArgumentError, "base_unity_url can not be nil" if base_unity_url.nil?
11
+ raise ArgumentError, "username can not be nil" if username.nil?
12
+ raise ArgumentError, "password can not be nil" if password.nil?
13
+ raise ArgumentError, "appname can not be nil" if appname.nil?
14
+
15
+ @base_unity_url = base_unity_url.gsub /\/$/, ""
16
+ @username = username
17
+ @password = password
18
+ @appname = appname
19
+ @proxy = proxy
20
+ @log = log
21
+
22
+ if logger.nil?
23
+ @logger = Logger.new(STDOUT)
24
+ @logger.level = Logger::INFO
25
+ else
26
+ @logger = logger
27
+ end
28
+
29
+ unless timezone.nil?
30
+ @timezone = Timezone.new(timezone)
31
+ else
32
+ @timezone = Timezone.new("UTC")
33
+ end
34
+ end
35
+
36
+ def security_token?
37
+ return !@security_token.nil?
38
+ end
39
+
40
+ def log?
41
+ return @log
42
+ end
43
+
44
+ def client_type
45
+ return :none
46
+ end
47
+
48
+ def magic(parameters = {})
49
+ raise NotImplementedError, "magic not implemented"
50
+ end
51
+
52
+ def get_security_token!(parameters = {})
53
+ raise NotImplementedError, "get_security_token! not implemented"
54
+ end
55
+
56
+ def retire_security_token!(parameters = {})
57
+ raise NotImplementedError, "retire_security_token! not implemented"
58
+ end
59
+
60
+ protected
61
+
62
+ def log_get_security_token
63
+ message = "Unity API GetSecurityToken request to #{@base_unity_url}"
64
+ log_info(message)
65
+ end
66
+
67
+ def log_retire_security_token
68
+ message = "Unity API RetireSecurityToken request to #{@base_unity_url}"
69
+ log_info(message)
70
+ end
71
+
72
+ def log_magic(request)
73
+ raise ArgumentError, "request can not be nil" if request.nil?
74
+ message = "Unity API Magic request to #{@base_unity_url} [#{request.parameters[:action]}]"
75
+ log_info(message)
76
+ end
77
+
78
+ def log_info(message)
79
+ if log? && !logger.nil? && !message.nil?
80
+ message += " #{@timer} seconds" unless @timer.nil?
81
+ @timer = nil
82
+ logger.info(message)
83
+ end
84
+ end
85
+
86
+ def start_timer
87
+ @start_time = Time.now.utc
88
+ end
89
+
90
+ def end_timer
91
+ @end_time = Time.now.utc
92
+ @timer = @end_time - @start_time
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,110 @@
1
+ require "json"
2
+ require "httpi"
3
+
4
+ module AllscriptsUnityClient
5
+ class JSONClientDriver < ClientDriver
6
+ attr_accessor :json_base_url
7
+
8
+ UNITY_JSON_ENDPOINT = "/Unity/UnityService.svc/json"
9
+
10
+ def initialize(base_unity_url, username, password, appname, proxy = nil, timezone = nil, logger = nil, log = true)
11
+ super
12
+
13
+ # Disable HTTPI logging
14
+ HTTPI.log = false
15
+
16
+ @json_base_url = "#{@base_unity_url}#{UNITY_JSON_ENDPOINT}"
17
+ end
18
+
19
+ def client_type
20
+ return :json
21
+ end
22
+
23
+ def magic(parameters = {})
24
+ request_data = JSONUnityRequest.new(parameters, @timezone, @appname, @security_token)
25
+ request = create_httpi_request("#{@json_base_url}/MagicJson", request_data.to_hash)
26
+
27
+ start_timer
28
+ response = HTTPI.post(request)
29
+ end_timer
30
+
31
+ response = JSON.parse(response.body)
32
+
33
+ raise_if_response_error(response)
34
+ log_magic(request_data)
35
+
36
+ response = JSONUnityResponse.new(response, @timezone)
37
+ response.to_hash
38
+ end
39
+
40
+ def get_security_token!(parameters = {})
41
+ username = parameters[:username] || @username
42
+ password = parameters[:password] || @password
43
+ appname = parameters[:appname] || @appname
44
+
45
+ request_data = {
46
+ "Username" => username,
47
+ "Password" => password,
48
+ "Appname" => appname
49
+ }
50
+ request = create_httpi_request("#{@json_base_url}/GetToken", request_data)
51
+
52
+ start_timer
53
+ response = HTTPI.post(request, :net_http_persistent)
54
+ end_timer
55
+
56
+ raise_if_response_error(response.body)
57
+ log_get_security_token
58
+
59
+ @security_token = response.body
60
+ end
61
+
62
+ def retire_security_token!(parameters = {})
63
+ token = parameters[:token] || @security_token
64
+ appname = parameters[:appname] || @appname
65
+
66
+ request_data = {
67
+ "Token" => token,
68
+ "Appname" => appname
69
+ }
70
+ request = create_httpi_request("#{@json_base_url}/RetireSecurityToken", request_data)
71
+
72
+ start_timer
73
+ response = HTTPI.post(request, :net_http_persistent)
74
+ end_timer
75
+
76
+ raise_if_response_error(response.body)
77
+ log_retire_security_token
78
+
79
+ @security_token = nil
80
+ end
81
+
82
+ private
83
+
84
+ def create_httpi_request(url, data)
85
+ request = HTTPI::Request.new
86
+ request.url = url
87
+ request.headers = {
88
+ "Accept-Encoding" => "gzip,deflate",
89
+ "Content-type" => "application/json;charset=utf-8"
90
+ }
91
+ request.body = JSON.generate(data)
92
+
93
+ unless @proxy.nil?
94
+ request.proxy = @proxy
95
+ end
96
+
97
+ request
98
+ end
99
+
100
+ def raise_if_response_error(response)
101
+ if response.nil?
102
+ raise APIError, "Response was empty"
103
+ elsif response.is_a?(Array) && !response[0]["Error"].nil?
104
+ raise APIError, response[0]["Error"]
105
+ elsif response.is_a?(String) && response.include?("error:")
106
+ raise APIError, response
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,33 @@
1
+ module AllscriptsUnityClient
2
+ class JSONUnityRequest < UnityRequest
3
+ def to_hash
4
+ action = @parameters[:action]
5
+ userid = @parameters[:userid]
6
+ appname = @parameters[:appname] || @appname
7
+ patientid = @parameters[:patientid]
8
+ token = @parameters[:token] || @security_token
9
+ parameter1 = process_date(@parameters[:parameter1]) || ""
10
+ parameter2 = process_date(@parameters[:parameter2]) || ""
11
+ parameter3 = process_date(@parameters[:parameter3]) || ""
12
+ parameter4 = process_date(@parameters[:parameter4]) || ""
13
+ parameter5 = process_date(@parameters[:parameter5]) || ""
14
+ parameter6 = process_date(@parameters[:parameter6]) || ""
15
+ data = Utilities::encode_data(@parameters[:data]) || ""
16
+
17
+ return {
18
+ "Action" => action,
19
+ "AppUserID" => userid,
20
+ "Appname" => appname,
21
+ "PatientID" => patientid,
22
+ "Token" => token,
23
+ "Parameter1" => parameter1,
24
+ "Parameter2" => parameter2,
25
+ "Parameter3" => parameter3,
26
+ "Parameter4" => parameter4,
27
+ "Parameter5" => parameter5,
28
+ "Parameter6" => parameter6,
29
+ "Data" => data
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ require "json"
2
+
3
+ module AllscriptsUnityClient
4
+ class JSONUnityResponse < UnityResponse
5
+ def to_hash
6
+ result = @response
7
+
8
+ # All JSON magic responses are an array with one item
9
+ result = result.first
10
+
11
+ # All JSON magic results contain one key on their object named
12
+ # actioninfo
13
+ result = result.values.first
14
+
15
+ # The data in a JSON magic result is always an array. If that array
16
+ # only has a single item, then just return that as the result. This is
17
+ # a compromise as some actions that should always return arrays
18
+ # (i.e. GetProviders) may return a single hash.
19
+ if result.count == 1
20
+ result = result.first
21
+ end
22
+
23
+ result = convert_dates_to_utc(result)
24
+ Utilities::recursively_symbolize_keys(result)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,128 @@
1
+ require "savon"
2
+
3
+ module AllscriptsUnityClient
4
+ class SOAPClientDriver < ClientDriver
5
+ attr_accessor :savon_client
6
+
7
+ UNITY_SOAP_ENDPOINT = "/Unity/UnityService.svc/unityservice"
8
+ UNITY_ENDPOINT_NAMESPACE = "http://www.allscripts.com/Unity/IUnityService"
9
+
10
+ def initialize(base_unity_url, username, password, appname, proxy = nil, timezone = nil, logger = nil, log = true)
11
+ super
12
+
13
+ client_proxy = @proxy
14
+ base_unity_url = "#{@base_unity_url}#{UNITY_SOAP_ENDPOINT}"
15
+
16
+ @savon_client = Savon.client do
17
+ # Removes the wsdl: namespace from body elements in the SOAP
18
+ # request. Unity doesn't recognize elements otherwise.
19
+ namespace_identifier nil
20
+
21
+ # Manually registers SOAP endpoint since Unity WSDL is not very
22
+ # good.
23
+ endpoint base_unity_url
24
+
25
+ # Manually register SOAP namespace. This URL isn't live, but the
26
+ # internal SOAP endpoints expect it.
27
+ namespace "http://www.allscripts.com/Unity"
28
+
29
+ # Register proxy with Savon if one was given.
30
+ unless client_proxy.nil?
31
+ proxy client_proxy
32
+ end
33
+
34
+ # Unity expects the SOAP envelop to be namespaced with soap:
35
+ env_namespace :soap
36
+
37
+ # Unity uses Microsoft style CamelCase for keys. Only really useful when using
38
+ # symbol keyed hashes.
39
+ convert_request_keys_to :camelcase
40
+
41
+ # Enable gzip on HTTP responses. Unity does not currently support this
42
+ # as of Born On 10/7/2013, but it doesn't hurt to future-proof. If gzip
43
+ # is ever enabled, this library will get a speed bump for free.
44
+ headers({ "Accept-Encoding" => "gzip,deflate" })
45
+
46
+ # Disable Savon logs
47
+ log false
48
+ end
49
+ end
50
+
51
+ def client_type
52
+ return :soap
53
+ end
54
+
55
+ def magic(parameters = {})
56
+ request_data = UnityRequest.new(parameters, @timezone, @appname, @security_token)
57
+ call_data = {
58
+ :message => request_data.to_hash,
59
+ :soap_action => "#{UNITY_ENDPOINT_NAMESPACE}/Magic"
60
+ }
61
+
62
+ begin
63
+ start_timer
64
+ response = @savon_client.call("Magic", call_data)
65
+ end_timer
66
+ rescue Savon::SOAPFault => e
67
+ raise APIError, e.message
68
+ end
69
+
70
+ log_magic(request_data)
71
+
72
+ response = UnityResponse.new(response.body, @timezone)
73
+ response.to_hash
74
+ end
75
+
76
+ def get_security_token!(parameters = {})
77
+ username = parameters[:username] || @username
78
+ password = parameters[:password] || @password
79
+ appname = parameters[:appname] || @appname
80
+
81
+ call_data = {
82
+ :message => {
83
+ "Username" => username,
84
+ "Password" => password,
85
+ "Appname" => appname
86
+ },
87
+ :soap_action => "#{UNITY_ENDPOINT_NAMESPACE}/GetSecurityToken"
88
+ }
89
+
90
+ begin
91
+ start_timer
92
+ response = @savon_client.call("GetSecurityToken", call_data)
93
+ end_timer
94
+ rescue Savon::SOAPFault => e
95
+ raise APIError, e.message
96
+ end
97
+
98
+ log_get_security_token
99
+
100
+ @security_token = response.body[:get_security_token_response][:get_security_token_result]
101
+ end
102
+
103
+ def retire_security_token!(parameters = {})
104
+ token = parameters[:token] || @security_token
105
+ appname = parameters[:appname] || @appname
106
+
107
+ call_data = {
108
+ :message => {
109
+ "Token" => token,
110
+ "Appname" => appname
111
+ },
112
+ :soap_action => "#{UNITY_ENDPOINT_NAMESPACE}/RetireSecurityToken"
113
+ }
114
+
115
+ begin
116
+ start_timer
117
+ @savon_client.call("RetireSecurityToken", call_data)
118
+ end_timer
119
+ rescue Savon::SOAPFault => e
120
+ raise APIError, e.message
121
+ end
122
+
123
+ log_retire_security_token
124
+
125
+ @security_token = nil
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,99 @@
1
+ require "date"
2
+ require "tzinfo"
3
+
4
+ module AllscriptsUnityClient
5
+ class Timezone
6
+ attr_accessor :tzinfo
7
+
8
+ def initialize(zone_identifier)
9
+ raise ArgumentError, "zone_identifier can not be nil" if zone_identifier.nil?
10
+
11
+ @tzinfo = TZInfo::Timezone.get(zone_identifier)
12
+ end
13
+
14
+ # Use TZInfo to convert a given UTC datetime into
15
+ # a local
16
+ def local_to_utc(datetime)
17
+ convert_with_timezone(:local_to_utc, datetime)
18
+ end
19
+
20
+ def utc_to_local(datetime = nil)
21
+ convert_with_timezone(:utc_to_local, datetime)
22
+ end
23
+
24
+ private
25
+
26
+ # Direction can be :utc_to_local or :local_to_utc
27
+ def convert_with_timezone(direction, datetime = nil)
28
+ if datetime.nil?
29
+ return nil
30
+ end
31
+
32
+ # Dates have no time information and so timezone conversion
33
+ # doesn't make sense. Just return the date in this case.
34
+ if datetime.instance_of?(Date)
35
+ return datetime
36
+ end
37
+
38
+ if datetime.instance_of?(String)
39
+ datetime = DateTime.parse(datetime.to_s)
40
+ end
41
+
42
+ is_datetime = datetime.instance_of?(DateTime)
43
+
44
+ if direction == :local_to_utc
45
+ if is_datetime
46
+ # DateTime can do UTC conversions reliably, so use that instead of
47
+ # TZInfo
48
+ datetime = datetime.new_offset(0)
49
+ else
50
+ datetime = @tzinfo.local_to_utc(datetime)
51
+ end
52
+
53
+ if is_datetime
54
+ # Convert to a DateTime with a UTC offset
55
+ datetime = DateTime.parse("#{datetime.strftime("%FT%T")}Z")
56
+ end
57
+ end
58
+
59
+ if direction == :utc_to_local
60
+ datetime = @tzinfo.utc_to_local(datetime)
61
+
62
+ if is_datetime
63
+ # Convert to a DateTime with the correct timezone offset
64
+ datetime = DateTime.parse(iso8601_with_offset(datetime))
65
+ end
66
+ end
67
+
68
+ return datetime
69
+ end
70
+
71
+ # TZInfo does not correctly update a DateTime's
72
+ # offset, so we manually format a ISO8601 with
73
+ # the correct format
74
+ def iso8601_with_offset(datetime)
75
+ if datetime.nil?
76
+ return nil
77
+ end
78
+
79
+ offset = @tzinfo.current_period.utc_offset
80
+ negative_offset = false
81
+ datetime_string = datetime.strftime("%FT%T")
82
+
83
+ if offset < 0
84
+ offset *= -1
85
+ negative_offset = true
86
+ end
87
+
88
+ if offset == 0
89
+ offset_string = "Z"
90
+ else
91
+ offset_string = Time.at(offset).utc.strftime("%H:%M")
92
+ offset_string = "-" + offset_string if negative_offset
93
+ offset_string = "+" + offset_string unless negative_offset
94
+ end
95
+
96
+ "#{datetime_string}#{offset_string}"
97
+ end
98
+ end
99
+ end