allscripts_unity_client 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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