smarts_api 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.
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ *.rbc
2
+ *.sassc
3
+ .sass-cache
4
+ capybara-*.html
5
+ .rspec
6
+ /.bundle
7
+ /vendor/bundle
8
+ /log/*
9
+ /tmp/*
10
+ /db/*.sqlite3
11
+ /public/system/*
12
+ /coverage/
13
+ /spec/tmp/*
14
+ **.orig
15
+ rerun.txt
16
+ pickle-email-*.html
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in smarts_api.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ smarts_api (0.0.1)
5
+ active_support
6
+ typhoeus
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ active_support (3.0.0)
12
+ activesupport (= 3.0.0)
13
+ activesupport (3.0.0)
14
+ addressable (2.3.5)
15
+ crack (0.4.1)
16
+ safe_yaml (~> 0.9.0)
17
+ diff-lcs (1.2.4)
18
+ ethon (0.6.1)
19
+ ffi (>= 1.3.0)
20
+ mime-types (~> 1.18)
21
+ ffi (1.9.0)
22
+ mime-types (1.25)
23
+ rspec (2.14.1)
24
+ rspec-core (~> 2.14.0)
25
+ rspec-expectations (~> 2.14.0)
26
+ rspec-mocks (~> 2.14.0)
27
+ rspec-core (2.14.5)
28
+ rspec-expectations (2.14.2)
29
+ diff-lcs (>= 1.1.3, < 2.0)
30
+ rspec-mocks (2.14.3)
31
+ safe_yaml (0.9.5)
32
+ timecop (0.6.3)
33
+ typhoeus (0.6.5)
34
+ ethon (~> 0.6.1)
35
+ webmock (1.13.0)
36
+ addressable (>= 2.2.7)
37
+ crack (>= 0.3.2)
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ rspec (~> 2.14.1)
44
+ smarts_api!
45
+ timecop
46
+ webmock
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Steve Mitchell
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ smarts_api
2
+ ==========
3
+
4
+ Ruby API for Sparkling Logic SMARTS
5
+
6
+
7
+ This project and it's creators, contributors, fans, and haters are in no way associated with Sparkling Logic, the innovator and creator of SMARTS.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ class SmartsApi::Error < StandardError
2
+
3
+ end
@@ -0,0 +1,45 @@
1
+ class SmartsApi::ConnectMessage < SmartsApi::Message
2
+
3
+ def send
4
+ logger.info "Connecting to #{uri}" if logger.respond_to?(:info)
5
+ response = Typhoeus::Request.post(uri,
6
+ :method => method,
7
+ :headers => {:Accept => "text/json"},
8
+ :body => make_form(request_params)
9
+ )
10
+ raise SmartsApi::Error, "Service connection failed. Recieved empty reply" if response.nil? || response.body.blank?
11
+ reply = JSON.parse(response.body)
12
+
13
+ raise SmartsApi::Error, "Connection failed. Received malformed response." unless reply["Header"] && reply["Header"]["SessionId"]
14
+
15
+ session = reply["Header"]["SessionId"]
16
+ raise SmartsApi::Error, "Connection failed. Did not receive session ID" if session == "00000000-0000-0000-0000-000000000000"
17
+
18
+ return session
19
+ end
20
+
21
+ def request_document
22
+ {:OperationId =>1 , :Header => {:DeploymentId => project_id}}.to_json
23
+ end
24
+
25
+ def request_params
26
+ params = {
27
+ :appId => app_id,
28
+ :pwd => pwd,
29
+ :reqData => request_document,
30
+ :reqTime => self.timestamp,
31
+ :userId => user_id,
32
+ :workspaceId => workspace_id
33
+ }
34
+ signature = {
35
+ :sign => sign_request(params)
36
+ }
37
+ return params.merge(signature)
38
+ end
39
+
40
+ def uri
41
+ "#{base_uri}connect"
42
+ end
43
+
44
+ end
45
+
@@ -0,0 +1,39 @@
1
+ class SmartsApi::DisconnectMessage < SmartsApi::Message
2
+
3
+
4
+ def send(session)
5
+ body = make_form(request_params(session))
6
+ logger.info "Disconnecting" if logger.respond_to?(:info)
7
+ response = Typhoeus::Request.post(uri,
8
+ :method => method,
9
+ :headers => {:Accept => "text/json"},
10
+ :body => body
11
+ )
12
+ raise SmartsApi::Error, "Service connection failed. Recieved empty reply" if response.nil? || response.body.blank?
13
+ reply = JSON.parse(response.body)
14
+
15
+ raise SmartsApi::Error, "Connection failed. Received malformed response." unless reply["Header"] && reply["Header"]["SessionId"]
16
+
17
+ session = reply["Header"]["SessionId"]
18
+ raise SmartsApi::Error, "Connection failed. Did not receive session ID" if session == "00000000-0000-0000-0000-000000000000"
19
+ return session
20
+ end
21
+
22
+ def request_params(session)
23
+ params = {
24
+ :appId => app_id,
25
+ :reqTime => timestamp,
26
+ :session => session
27
+ }
28
+ signature = {
29
+ :sign => sign_request(params)
30
+ }
31
+ return params.merge(signature)
32
+ end
33
+
34
+ def uri
35
+ "#{base_uri}disconnect"
36
+ end
37
+
38
+
39
+ end
@@ -0,0 +1,51 @@
1
+ class SmartsApi::EvaluateMessage < SmartsApi::Message
2
+
3
+ def send(session, member, decision)
4
+ body = make_form(request_params(session, member, decision))
5
+ logger.info "Evaluating" if logger.respond_to?(:info)
6
+ response = Typhoeus::Request.post(uri,
7
+ :method => method,
8
+ :headers => {:Accept => "text/json"},
9
+ :body => body
10
+ )
11
+ raise SmartsApi::Error, "Rules Evaluation failed failed. Recieved empty reply" if response.nil? || response.body.blank?
12
+ reply = JSON.parse(response.body)
13
+
14
+ raise SmartsApi::Error, "Rules Evaluation failed failed. Received malformed response." unless reply["Header"] && ["Body"]
15
+
16
+ body = reply["Body"]
17
+
18
+ if body.blank?
19
+ logger.info "Rules Engine Evaluation failed. \n\n #{body} \n\n #{response.body}" if logger.respond_to?(:info)
20
+
21
+ raise SmartsApi::Error, "Rules Evaluation failed. Returned JSON is blank."
22
+ end
23
+
24
+ logger.info "Updating issues" if logger.respond_to?(:info)
25
+ member.process_smarts_response body
26
+ return body
27
+ end
28
+
29
+ def request_params(session, member, decision)
30
+ params = {
31
+ :appId => app_id,
32
+ :reqData => request_document(session, member, decision),
33
+ :reqTime => timestamp,
34
+ :session => session
35
+ }
36
+ signature = {
37
+ :sign => sign_request(params)
38
+ }
39
+ return params.merge(signature)
40
+ end
41
+
42
+ def request_document(session, member, decision)
43
+ doc = {:OperationId =>1 , :Header => {:SessionId => session, :DecisionId => decision}, :Body => {:Documents => []}}
44
+ doc[:Body][:Documents] = member.smarts_document
45
+ return doc.to_json
46
+ end
47
+
48
+ def uri
49
+ "#{base_uri}evaluate"
50
+ end
51
+ end
@@ -0,0 +1,81 @@
1
+ require 'typhoeus'
2
+ require 'json'
3
+ require 'active_support/core_ext/class'
4
+ require 'active_support/core_ext/array'
5
+ include ERB::Util
6
+ require 'openssl'
7
+ require 'base64'
8
+
9
+ class SmartsApi::Message
10
+ attr_reader :logger
11
+ cattr_accessor :user_id, :pwd, :app_id, :workspace_id, :access_key, :base_uri, :project_id
12
+
13
+
14
+ def initialize(logger=nil)
15
+ @logger = logger
16
+ end
17
+
18
+ def method
19
+ :post
20
+ end
21
+
22
+ def uri
23
+ base_uri
24
+ end
25
+
26
+ def sign_request(params)
27
+ #Below you will find the reverse-engineered ruby implementation of Sparkling Logic's security signing algorithm.
28
+ #this was translated from a javascript library on their documentation site, then
29
+
30
+ #The params object include the following parameters, in this order: AppId, pwd, reqData, reqTime, userID, and workspaceID.
31
+ #naming of the parameters is significant, and case-sensitive.
32
+
33
+ #combine the parameters into a long encoded string (again, order of elements is critical)
34
+ encoded_parts = []
35
+ http = URI.parse(uri)
36
+ encoded_parts[0] = method.to_s.upcase
37
+ encoded_parts[1] = http.host
38
+ encoded_parts[2] = http.request_uri
39
+ encoded_parts[3] = encode_hash(params)
40
+
41
+ encoded = encoded_parts.join("\n") #Join parts with newline character
42
+
43
+ #5 Digest the encode string with SHA256, using the pre-shared AccessKey as the digest key.
44
+ digest = OpenSSL::Digest::Digest.new('sha256')
45
+ hash = OpenSSL::HMAC.hexdigest(digest, access_key, encoded)
46
+
47
+ #6 OpenSSL::Digest correctly translates the Hash to a string of bytes. Sparkling Logic's algorithm ...unexpectedle converts the hex value to the corresponding ascii code.
48
+ #So we need to iterate each pair in our byte string and convert to ascii. this is a little ugly, but necessary
49
+ #7 AND THEN we encode that string to base64. Not sure why, exactly...
50
+ signature = Base64.encode64(hex_string_to_ascii(hash))
51
+ #8 re-URL_encode the string and remove line-endings. Again, for absolutely no logical reason. I think the javascript version did this automatically.
52
+ signature = url_encode(signature)
53
+ signature = signature.gsub("%0A", "" )
54
+ return signature
55
+
56
+ end
57
+
58
+
59
+ def hex_string_to_ascii hex_str
60
+ #we need to iterate our hex-string and convert each pair to a hex-number. Then evaluate that as ascii code and replace with the corresponding ascii character.
61
+ ascii_str = ''
62
+ hex_str.split('').in_groups_of(2){|c| ascii_str << (c[0]+c[1]).hex.chr }
63
+ ascii_str
64
+ end
65
+
66
+ def timestamp
67
+ Time.now.utc.iso8601.to_s # => "2012-06-21T18:15:09Z"
68
+ end
69
+
70
+ def encode_hash(hash)
71
+ new = []
72
+ hash.each do |k,v|
73
+ new << k.to_s+"="+url_encode(v)
74
+ end
75
+ return new.join("&")
76
+ end
77
+
78
+ def make_form(request_params)
79
+ request_params.map{|k,v| "#{k}=#{v}"}.join("&")
80
+ end
81
+ end
@@ -0,0 +1,5 @@
1
+ module SmartsApi
2
+ module Version # :nodoc:
3
+ STRING = '0.0.1'
4
+ end
5
+ end
data/lib/smarts_api.rb ADDED
@@ -0,0 +1,17 @@
1
+
2
+ module SmartsApi
3
+ Dir[File.expand_path('../../lib/smarts_api/*.rb', __FILE__)].each {|f| require f}
4
+ Dir[File.expand_path('../../lib/smarts_api/*/*.rb', __FILE__)].each {|f| require f}
5
+
6
+ def self.evaluate(decision, obj, logger = nil)
7
+ raise SmartsApi::Error.new("Object to be evaluated must define a method 'smarts_document'") unless obj.respond_to?(:smarts_document)
8
+ logger.info "processing request for #{obj}" if logger.respond_to?(:info)
9
+
10
+ session = SmartsApi::ConnectMessage.new(logger).send
11
+ response = SmartsApi::EvaluateMessage.new(logger).
12
+ send(session, obj, decision)
13
+
14
+ #SmartsApi::DisconnectMessage.new(logger).disconnect(session)
15
+ end
16
+
17
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/smarts_api/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Steve Mitchell"]
6
+ gem.email = ["theSteveMitchell@gmail.com"]
7
+ gem.description = "smarts_api-#{SmartsApi::Version::STRING}"
8
+ gem.summary = "api gem for Sparkling Logic's SMARTS business logic platform"
9
+ gem.homepage = "http://github.com/theSteveMitchell/smarts_api"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "smarts_api"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = SmartsApi::Version::STRING
17
+
18
+ gem.add_dependency('typhoeus')
19
+ gem.add_dependency('activesupport')
20
+
21
+ gem.add_development_dependency('rspec', '~> 2.14.1')
22
+ gem.add_development_dependency('webmock')
23
+ gem.add_development_dependency('timecop')
24
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe SmartsApi::ConnectMessage do
4
+ before (:all) do
5
+ SmartsApi::Message.base_uri = "http://smarts.dev.thismashine.com/"
6
+ SmartsApi::Message.access_key = "sshhhh...Secret!"
7
+ end
8
+
9
+ it 'should return session ID if successful' do
10
+
11
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}connect")
12
+ .to_return(:status => 200, :body => "{\"Header\":{\"SessionId\":\"487d2c44-43fe-44d3-988f-ea462af03169\"},\"Body\":null,\"ErrorInfo\":null,\"Metrics\":null,\"Success\":true,\"OperationException\":null}")
13
+
14
+
15
+ SmartsApi::ConnectMessage.new().send.should == "487d2c44-43fe-44d3-988f-ea462af03169"
16
+
17
+ end
18
+
19
+ it 'should throw an error if the connection returns an error' do
20
+
21
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}connect")
22
+ .to_return(:status => 200, :body => "{\"OperationId\":0,\"Header\":{\"SessionId\":\"00000000-0000-0000-0000-000000000000\",\"TransactionTime\":\"2012-06-22T21:02:16.642625Z\",\"Workspace\":null,\"DeploymentId\":null,\"DecisionId\":null},\"Body\":null,\"ErrorInfo\":{\"ErrorCode\":\"ServerException\",\"ErrorMessage\":\"Exception during connection\",\"Details\":[\"Invalid API access\"]},\"Metrics\":null,\"Success\":false,\"OperationException\":{\"IsRestException\":true,\"ErrorType\":\"DocApiAccessDeniedException\",\"CompleteStackTrace\":\"Type: DocApiAccessDeniedException\\r\\nMessage: Invalid API access\\r\\nStack Trace:\\r\\n at Splog.Rest.Base.DocRestHttpHandler.VerifyHmacSignature(String method, IEnumerable`1 keys, String signature, String[] paramaters)\\r\\n at Splog.Rest.Decisions.DocRestDecisionService.Connect(String appId, String reqTime, String userId, String pwd, String workspaceId, String reqData, String sign)\\r\\n\\r\\n\",\"ExtraInfo\":null,\"Message\":\"[DecisionServer] Exception connecting to the decision server for session 72950db0-9639-477b-b824-b82ad5122b56\",\"Data\":{}}}")
23
+
24
+ expect{SmartsApi::ConnectMessage.new().send}.to raise_error(SmartsApi::Error)
25
+
26
+ end
27
+
28
+ it 'should throw an error if the returned sessionID is empty' do
29
+
30
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}connect")
31
+ .to_return(:status => 200, :body => "{\"OperationId\":0,\"Header\":{\"SessionId\":\"00000000-0000-0000-0000-000000000000\",\"TransactionTime\":\"2012-06-22T21:02:16.642625Z\",\"Workspace\":null,\"DeploymentId\":null,\"DecisionId\":null},\"Body\":null,\"ErrorInfo\":{\"ErrorCode\":\"ServerException\",\"ErrorMessage\":\"Exception during connection\",\"Details\":[\"Invalid API access\"]},\"Metrics\":null,\"Success\":false,\"OperationException\":{\"IsRestException\":true,\"ErrorType\":\"DocApiAccessDeniedException\",\"CompleteStackTrace\":\"Type: DocApiAccessDeniedException\\r\\nMessage: Invalid API access\\r\\nStack Trace:\\r\\n at Splog.Rest.Base.DocRestHttpHandler.VerifyHmacSignature(String method, IEnumerable`1 keys, String signature, String[] paramaters)\\r\\n at Splog.Rest.Decisions.DocRestDecisionService.Connect(String appId, String reqTime, String userId, String pwd, String workspaceId, String reqData, String sign)\\r\\n\\r\\n\",\"ExtraInfo\":null,\"Message\":\"[DecisionServer] Exception connecting to the decision server for session 72950db0-9639-477b-b824-b82ad5122b56\",\"Data\":{}}}")
32
+
33
+ expect{SmartsApi::ConnectMessage.new().send}.to raise_error(SmartsApi::Error)
34
+
35
+ end
36
+
37
+ it 'should throw an error if no body is returned' do
38
+
39
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}connect")
40
+ .to_return(:status => 200)
41
+ expect{SmartsApi::ConnectMessage.new().send}.to raise_error(SmartsApi::Error)
42
+
43
+ end
44
+
45
+ it 'should throw an error if status code is bad' do
46
+
47
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}connect")
48
+ .to_return(:status => 500)
49
+ expect{SmartsApi::ConnectMessage.new().send}.to raise_error(SmartsApi::Error)
50
+
51
+ end
52
+
53
+
54
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe SmartsApi::DisconnectMessage do
5
+ it 'should return session ID if successful' do
6
+
7
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}disconnect")
8
+ .to_return(:status => 200, :body => "{\"Header\":{\"SessionId\":\"487d2c44-43fe-44d3-988f-ea462af03169\"},\"Body\":null,\"ErrorInfo\":null,\"Metrics\":null,\"Success\":true,\"OperationException\":null}")
9
+
10
+
11
+ SmartsApi::DisconnectMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169").should == "487d2c44-43fe-44d3-988f-ea462af03169"
12
+
13
+ end
14
+
15
+ it 'should throw an error if the connection returns an error' do
16
+
17
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}disconnect")
18
+ .to_return(:status => 200, :body => "{\"OperationId\":0,\"Header\":{\"SessionId\":\"00000000-0000-0000-0000-000000000000\",\"TransactionTime\":\"2012-06-22T21:02:16.642625Z\",\"Workspace\":null,\"DeploymentId\":null,\"DecisionId\":null},\"Body\":null,\"ErrorInfo\":{\"ErrorCode\":\"ServerException\",\"ErrorMessage\":\"Exception during connection\",\"Details\":[\"Invalid API access\"]},\"Metrics\":null,\"Success\":false,\"OperationException\":{\"IsRestException\":true,\"ErrorType\":\"DocApiAccessDeniedException\",\"CompleteStackTrace\":\"Type: DocApiAccessDeniedException\\r\\nMessage: Invalid API access\\r\\nStack Trace:\\r\\n at Splog.Rest.Base.DocRestHttpHandler.VerifyHmacSignature(String method, IEnumerable`1 keys, String signature, String[] paramaters)\\r\\n at Splog.Rest.Decisions.DocRestDecisionService.Connect(String appId, String reqTime, String userId, String pwd, String workspaceId, String reqData, String sign)\\r\\n\\r\\n\",\"ExtraInfo\":null,\"Message\":\"[DecisionServer] Exception connecting to the decision server for session 72950db0-9639-477b-b824-b82ad5122b56\",\"Data\":{}}}")
19
+
20
+ expect{SmartsApi::DisconnectMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169")}.to raise_error(SmartsApi::Error)
21
+
22
+ end
23
+
24
+ it 'should throw an error if the returned sessionID is empty returns an error' do
25
+
26
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}disconnect")
27
+ .to_return(:status => 200, :body => "{\"OperationId\":0,\"Header\":{\"SessionId\":\"00000000-0000-0000-0000-000000000000\",\"TransactionTime\":\"2012-06-22T21:02:16.642625Z\",\"Workspace\":null,\"DeploymentId\":null,\"DecisionId\":null},\"Body\":null,\"ErrorInfo\":{\"ErrorCode\":\"ServerException\",\"ErrorMessage\":\"Exception during connection\",\"Details\":[\"Invalid API access\"]},\"Metrics\":null,\"Success\":false,\"OperationException\":{\"IsRestException\":true,\"ErrorType\":\"DocApiAccessDeniedException\",\"CompleteStackTrace\":\"Type: DocApiAccessDeniedException\\r\\nMessage: Invalid API access\\r\\nStack Trace:\\r\\n at Splog.Rest.Base.DocRestHttpHandler.VerifyHmacSignature(String method, IEnumerable`1 keys, String signature, String[] paramaters)\\r\\n at Splog.Rest.Decisions.DocRestDecisionService.Connect(String appId, String reqTime, String userId, String pwd, String workspaceId, String reqData, String sign)\\r\\n\\r\\n\",\"ExtraInfo\":null,\"Message\":\"[DecisionServer] Exception connecting to the decision server for session 72950db0-9639-477b-b824-b82ad5122b56\",\"Data\":{}}}")
28
+
29
+ expect{SmartsApi::DisconnectMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169")}.to raise_error(SmartsApi::Error)
30
+
31
+ end
32
+
33
+ it 'should throw an error if no body is returned' do
34
+
35
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}disconnect")
36
+ .to_return(:status => 200)
37
+ expect{SmartsApi::DisconnectMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169")}.to raise_error(SmartsApi::Error)
38
+
39
+ end
40
+
41
+ it 'should throw an error if status code is bad' do
42
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}disconnect")
43
+ .to_return(:status => 500)
44
+ expect{SmartsApi::DisconnectMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169")}.to raise_error(SmartsApi::Error)
45
+
46
+ end
47
+
48
+
49
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe SmartsApi::EvaluateMessage do
4
+
5
+ before (:all) do
6
+ SmartsApi::Message.base_uri = "http://smarts.dev.thismashine.com/"
7
+ SmartsApi::Message.access_key = "sshhhh...Secret!"
8
+ end
9
+
10
+ let(:eval_class) {
11
+ Class.new() do
12
+
13
+ def smarts_document
14
+ "123"
15
+ end
16
+
17
+ def process_smarts_response(body)
18
+
19
+ end
20
+ end
21
+ }
22
+
23
+ let(:eval_object) {
24
+ eval_class.new()
25
+ }
26
+
27
+ it 'should return session ID if successful' do
28
+
29
+ body = "{\"OperationId\":1,\"Header\":{\"SessionId\":\"e3e2b012-e9b6-45e7-a96a-a009ebf0a07a\"},\"Body\":{\"Documents\":[{\"identification\":{\"actor_group\":\"root\",\"actor_id\":3,\"gender\":\"Male\",\"birth_date\":\"1984-12-08T06:00:00Z\"},\"historical_properties\":[{\"weight\":\"150\"}]}]},\"ErrorInfo\":null,\"Metrics\":null,\"Success\":true,\"OperationException\":null}"
30
+
31
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}evaluate")
32
+ .to_return(:status => 200, :body => body)
33
+ SmartsApi::EvaluateMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169", eval_object, "Issues Analysis Decision").should == JSON.parse(body)["Body"]
34
+
35
+ end
36
+
37
+ it 'should throw an error if the connection returns an error' do
38
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}evaluate")
39
+ .to_return(:status => 200, :body => "{\"OperationId\":0,\"Header\":{\"SessionId\":\"00000000-0000-0000-0000-000000000000\",\"TransactionTime\":\"2012-06-22T21:02:16.642625Z\",\"Workspace\":null,\"DeploymentId\":null,\"DecisionId\":null},\"Body\":null,\"ErrorInfo\":{\"ErrorCode\":\"ServerException\",\"ErrorMessage\":\"Exception during connection\",\"Details\":[\"Invalid API access\"]},\"Metrics\":null,\"Success\":false,\"OperationException\":{\"IsRestException\":true,\"ErrorType\":\"DocApiAccessDeniedException\",\"CompleteStackTrace\":\"Type: DocApiAccessDeniedException\\r\\nMessage: Invalid API access\\r\\nStack Trace:\\r\\n at Splog.Rest.Base.DocRestHttpHandler.VerifyHmacSignature(String method, IEnumerable`1 keys, String signature, String[] paramaters)\\r\\n at Splog.Rest.Decisions.DocRestDecisionService.Connect(String appId, String reqTime, String userId, String pwd, String workspaceId, String reqData, String sign)\\r\\n\\r\\n\",\"ExtraInfo\":null,\"Message\":\"[DecisionServer] Exception connecting to the decision server for session 72950db0-9639-477b-b824-b82ad5122b56\",\"Data\":{}}}")
40
+ expect{SmartsApi::EvaluateMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169",eval_object, "Issues Analysis Decision")}.to raise_error(SmartsApi::Error)
41
+
42
+ end
43
+
44
+ it 'should throw an error if the returned sessionID is empty returns an error' do
45
+
46
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}evaluate")
47
+ .to_return(:status => 200, :body => "{\"OperationId\":0,\"Header\":{\"SessionId\":\"00000000-0000-0000-0000-000000000000\",\"TransactionTime\":\"2012-06-22T21:02:16.642625Z\",\"Workspace\":null,\"DeploymentId\":null,\"DecisionId\":null},\"Body\":null,\"ErrorInfo\":{\"ErrorCode\":\"ServerException\",\"ErrorMessage\":\"Exception during connection\",\"Details\":[\"Invalid API access\"]},\"Metrics\":null,\"Success\":false,\"OperationException\":{\"IsRestException\":true,\"ErrorType\":\"DocApiAccessDeniedException\",\"CompleteStackTrace\":\"Type: DocApiAccessDeniedException\\r\\nMessage: Invalid API access\\r\\nStack Trace:\\r\\n at Splog.Rest.Base.DocRestHttpHandler.VerifyHmacSignature(String method, IEnumerable`1 keys, String signature, String[] paramaters)\\r\\n at Splog.Rest.Decisions.DocRestDecisionService.Connect(String appId, String reqTime, String userId, String pwd, String workspaceId, String reqData, String sign)\\r\\n\\r\\n\",\"ExtraInfo\":null,\"Message\":\"[DecisionServer] Exception connecting to the decision server for session 72950db0-9639-477b-b824-b82ad5122b56\",\"Data\":{}}}")
48
+ expect{SmartsApi::EvaluateMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169",eval_object, "Issues Analysis Decision")}.to raise_error(SmartsApi::Error)
49
+
50
+ end
51
+
52
+ it 'should throw an error if no body is returned' do
53
+
54
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}evaluate")
55
+ .to_return(:status => 200)
56
+ expect{SmartsApi::EvaluateMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169",eval_object, "Issues Analysis Decision")}.to raise_error(SmartsApi::Error)
57
+
58
+ end
59
+
60
+ it 'should throw an error if status code is bad' do
61
+
62
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}evaluate")
63
+ .to_return(:status => 500)
64
+ expect{SmartsApi::EvaluateMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169",eval_object, "Issues Analysis Decision")}.to raise_error(SmartsApi::Error)
65
+
66
+ end
67
+
68
+ it 'should call the document method on the member object' do
69
+ eval_object.should_receive(:smarts_document)
70
+ body = "{\"OperationId\":1,\"Header\":{\"SessionId\":\"e3e2b012-e9b6-45e7-a96a-a009ebf0a07a\"},\"Body\":{\"Documents\":[{\"identification\":{\"actor_group\":\"root\",\"actor_id\":3,\"gender\":\"Male\",\"birth_date\":\"1984-12-08T06:00:00Z\"},\"historical_properties\":[{\"weight\":\"150\"}]}]},\"ErrorInfo\":null,\"Metrics\":null,\"Success\":true,\"OperationException\":null}"
71
+
72
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}evaluate")
73
+ .to_return(:status => 200, :body => body)
74
+ SmartsApi::EvaluateMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169", eval_object, "Issues Analysis Decision").should == JSON.parse(body)["Body"]
75
+ end
76
+
77
+ it "should pass the returned message body to the evaluated object for processing" do
78
+
79
+ body = "{\"OperationId\":1,\"Header\":{\"SessionId\":\"e3e2b012-e9b6-45e7-a96a-a009ebf0a07a\"},\"Body\":{\"Documents\":[{\"identification\":{\"actor_group\":\"root\",\"actor_id\":3,\"gender\":\"Male\",\"birth_date\":\"1984-12-08T06:00:00Z\"},\"historical_properties\":[{\"weight\":\"150\"}]}]},\"ErrorInfo\":null,\"Metrics\":null,\"Success\":true,\"OperationException\":null}"
80
+ eval_object.should_receive(:process_smarts_response).with(JSON.parse(body)["Body"])
81
+ stub_http_request(:post, "#{SmartsApi::Message.base_uri}evaluate")
82
+ .to_return(:status => 200, :body => body)
83
+ SmartsApi::EvaluateMessage.new().send("487d2c44-43fe-44d3-988f-ea462af03169", eval_object, "Issues Analysis Decision").should == JSON.parse(body)["Body"]
84
+ end
85
+
86
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe SmartsApi::Message do
4
+
5
+ before (:all) do
6
+ SmartsApi::Message.base_uri = "http://www.versign.com/request/doSomething.aspc"
7
+ SmartsApi::Message.access_key = "secretKey"
8
+ end
9
+
10
+ describe 'initializer' do
11
+ it "should keep the logger for logging purposes" do
12
+ logger = Logger.new(STDOUT)
13
+ message = SmartsApi::Message.new(logger)
14
+
15
+ message.logger.should == logger
16
+
17
+ end
18
+ end
19
+
20
+ describe 'timestamp' do
21
+ it "should be formatted correctly" do
22
+ Timecop.freeze(Time.utc(2012, 6, 21, 12, 34, 56))
23
+ SmartsApi::Message.new().timestamp.should == "2012-06-21T12:34:56Z"
24
+ Timecop.return
25
+
26
+ end
27
+ end
28
+
29
+ describe 'hash encoding' do
30
+ it 'should convert a hash into a url_encoded string' do
31
+ hash = {:a => "123", :b => "aber=/3sd8:&++\\", :c => "hellpo"}
32
+
33
+ SmartsApi::Message.new().encode_hash(hash).should == "a=123&b=aber%3D%2F3sd8%3A%26%2B%2B%5C&c=hellpo"
34
+
35
+ end
36
+
37
+ end
38
+
39
+ describe 'ascii conversion' do
40
+ it "should convert a hex string to ascii in the way the service expects" do
41
+ #even if it's wrong...'
42
+
43
+ SmartsApi::Message.new().hex_string_to_ascii("3D").should == "="
44
+ SmartsApi::Message.new().hex_string_to_ascii("0B").should == "\v"
45
+ SmartsApi::Message.new().hex_string_to_ascii("52").should == "R"
46
+
47
+ hex = "52410baf20302e9e2c31b5ecea597348aa3df3067c58cf35cb5bd94273dc1de0"
48
+ SmartsApi::Message.new().hex_string_to_ascii(hex)
49
+ .should == "RA\v\xAF 0.\x9E,1\xB5\xEC\xEAYsH\xAA=\xF3\x06|X\xCF5\xCB[\xD9Bs\xDC\x1D\xE0"
50
+ end
51
+
52
+
53
+ end
54
+
55
+ #Notes on signing: The signing algorithm for Sparkling Logic is very brittle and depends on ALL of the
56
+ #values in sparkling_logic.yml, as well as timestamp. The below tests will fail if any of those settings change.
57
+ #so if the signature does not match the expected, and one of those settings did change, we can update the expected sig.
58
+ describe 'signing' do
59
+ it "should create a correct signature when no params supplied" do
60
+ Timecop.freeze(Time.utc(2012, 6, 21, 12, 34, 56))
61
+
62
+ SmartsApi::Message.new().sign_request({})
63
+ .should == "dOqhdTRLwMSUBSO7HZDRTsD63fVVA%2FKnffCn3DuaRnE%3D"
64
+ Timecop.return
65
+ end
66
+
67
+ it "should create a correct signature when params supplied" do
68
+ Timecop.freeze(Time.utc(2012, 6, 21, 12, 34, 56))
69
+ params = {:param1 => "nothing", :param2 => "nothing More"}
70
+
71
+ SmartsApi::Message.new().sign_request(params)
72
+ .should == "EIvlfFrm7FpotCVp6MIt76P6bz%2BUKWbVaO7pAIclyd8%3D"
73
+ Timecop.return
74
+ end
75
+
76
+ end
77
+
78
+
79
+
80
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe SmartsApi do
4
+
5
+ describe "evaluate" do
6
+
7
+ before (:all) do
8
+ SmartsApi::Message.base_uri = "http://smarts.dev.thismashine.com/"
9
+ SmartsApi::Message.access_key = "sshhhh...Secret!"
10
+ end
11
+
12
+ describe "expectations on eval object" do
13
+
14
+ it 'should raise error and not call connect if obj is nil' do
15
+ expect{SmartsApi.evaluate(
16
+ "string", nil
17
+ )}.to raise_error(SmartsApi::Error, "Object to be evaluated must define a method 'smarts_document'")
18
+ end
19
+
20
+ it 'should raise error and not call connect if obj does not define the document method' do
21
+ RegularClass = Class.new() {}
22
+ instance = RegularClass.new
23
+ instance.should_not be_nil
24
+ expect{SmartsApi.evaluate(
25
+ "string", instance
26
+ )}.to raise_error(SmartsApi::Error, "Object to be evaluated must define a method 'smarts_document'")
27
+ end
28
+
29
+ it 'should accept any object that defines the document method' do
30
+ EvalClass= Class.new() do
31
+ def smarts_document
32
+ "EvalClass.Instance 1"
33
+ end
34
+ end
35
+
36
+ instance = EvalClass.new
37
+ instance.should_not be_nil
38
+
39
+ SmartsApi::ConnectMessage.any_instance.should_receive(:send).and_return("session 334")
40
+ SmartsApi::EvaluateMessage.any_instance.should_receive(:send)
41
+ SmartsApi.evaluate("string", instance)
42
+ end
43
+ end
44
+
45
+ describe "specifying decision string" do
46
+ it "should include the decision name and session when sending the evaluate message" do
47
+
48
+ instance = EvalClass.new
49
+ instance.should_not be_nil
50
+ SmartsApi::ConnectMessage.any_instance.should_receive(:send).and_return("session 3339")
51
+ SmartsApi::EvaluateMessage.any_instance.should_receive(:send).with("session 3339", instance, "Chosen_decision")
52
+ SmartsApi.evaluate("Chosen_decision", instance)
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+ end
@@ -0,0 +1,3 @@
1
+ require 'webmock/rspec'
2
+ require 'timecop'
3
+ Dir[File.expand_path('../../lib/smarts_api.rb', __FILE__)].each {|f| require f}
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smarts_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steve Mitchell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: typhoeus
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.14.1
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.14.1
62
+ - !ruby/object:Gem::Dependency
63
+ name: webmock
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: timecop
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: smarts_api-0.0.1
95
+ email:
96
+ - theSteveMitchell@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - Gemfile
103
+ - Gemfile.lock
104
+ - LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - lib/smarts_api.rb
108
+ - lib/smarts_api/error.rb
109
+ - lib/smarts_api/message.rb
110
+ - lib/smarts_api/message/connect_message.rb
111
+ - lib/smarts_api/message/disconnect_message.rb
112
+ - lib/smarts_api/message/evaluate_message.rb
113
+ - lib/smarts_api/version.rb
114
+ - smarts_api.gemspec
115
+ - spec/smarts_api/message/connect_message_spec.rb
116
+ - spec/smarts_api/message/disconnect_message_spec.rb
117
+ - spec/smarts_api/message/evaluate_message_spec.rb
118
+ - spec/smarts_api/message_spec.rb
119
+ - spec/smarts_api_spec.rb
120
+ - spec/spec_helper.rb
121
+ homepage: http://github.com/theSteveMitchell/smarts_api
122
+ licenses: []
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ! '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 1.8.24
142
+ signing_key:
143
+ specification_version: 3
144
+ summary: api gem for Sparkling Logic's SMARTS business logic platform
145
+ test_files:
146
+ - spec/smarts_api/message/connect_message_spec.rb
147
+ - spec/smarts_api/message/disconnect_message_spec.rb
148
+ - spec/smarts_api/message/evaluate_message_spec.rb
149
+ - spec/smarts_api/message_spec.rb
150
+ - spec/smarts_api_spec.rb
151
+ - spec/spec_helper.rb