api_ai_wrapper 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 469724d657925c38ac67bf0c138f213735ed032a
4
+ data.tar.gz: ccc0bd55f2401a5a57766bb53fd030076a528b52
5
+ SHA512:
6
+ metadata.gz: 3b58704d1d01e1e063e562d349d6dd2379256d52f192c5298616ff0f6a98bb2ca5644beff1fc184abc3bab00ac28c67acd4064f93de4b9c0969f8135fce5891f
7
+ data.tar.gz: a941104c2cb448f049048fe366877863efff846252b3523b62a06b4c9e28e8f085b924872dddaabfe7d603f8781153f44cae6c356a72286d192477ff0c6e9771
data/.gitignore ADDED
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ # Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ #encoding: utf-8
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem "httpclient"
6
+
7
+ group :test, :development do
8
+ gem "rspec"
9
+ gem "simplecov"
10
+ gem "webmock"
11
+ end
12
+
13
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.5.1)
5
+ public_suffix (~> 2.0, >= 2.0.2)
6
+ crack (0.4.3)
7
+ safe_yaml (~> 1.0.0)
8
+ diff-lcs (1.3)
9
+ docile (1.1.5)
10
+ hashdiff (0.3.4)
11
+ httpclient (2.8.3)
12
+ json (2.1.0)
13
+ public_suffix (2.0.5)
14
+ rspec (3.6.0)
15
+ rspec-core (~> 3.6.0)
16
+ rspec-expectations (~> 3.6.0)
17
+ rspec-mocks (~> 3.6.0)
18
+ rspec-core (3.6.0)
19
+ rspec-support (~> 3.6.0)
20
+ rspec-expectations (3.6.0)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.6.0)
23
+ rspec-mocks (3.6.0)
24
+ diff-lcs (>= 1.2.0, < 2.0)
25
+ rspec-support (~> 3.6.0)
26
+ rspec-support (3.6.0)
27
+ safe_yaml (1.0.4)
28
+ simplecov (0.14.1)
29
+ docile (~> 1.1.0)
30
+ json (>= 1.8, < 3)
31
+ simplecov-html (~> 0.10.0)
32
+ simplecov-html (0.10.1)
33
+ webmock (3.0.1)
34
+ addressable (>= 2.3.6)
35
+ crack (>= 0.3.2)
36
+ hashdiff
37
+
38
+ PLATFORMS
39
+ ruby
40
+
41
+ DEPENDENCIES
42
+ httpclient
43
+ rspec
44
+ simplecov
45
+ webmock
46
+
47
+ BUNDLED WITH
48
+ 1.15.3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # api_ai_wrapper
2
+ A simple library that let's any developer automate the training process of a Natural Language Processing Engine on API.AI, and retrieve meaning from new utterances.
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'api_ai_wrapper'
3
+ s.version = '1.0.0'
4
+ s.date = '2017-08-04'
5
+ s.summary = "An API.AI Ruby Wrapper"
6
+ s.description = "A simple ruby library that let's any developer automate the training process of a Natural Language Processing Engine on API.AI, and retrieve meaning from new utterances."
7
+ s.authors = ["Vincent Gabou"]
8
+ s.email = 'vincent.gabou@gmail.com'
9
+ s.files = `git ls-files`.split($/)
10
+ s.homepage = 'http://rubygems.org/gems/api_ai_wrapper'
11
+ s.license = 'MIT'
12
+
13
+ s.add_runtime_dependency 'httpclient', '~> 2.8', '>= 2.8.0'
14
+ end
@@ -0,0 +1,10 @@
1
+ require "json"
2
+ require "httpclient"
3
+ require "securerandom"
4
+
5
+ require "api_ai_wrapper/extensions/object"
6
+ require "api_ai_wrapper/extensions/string"
7
+ require "api_ai_wrapper/extensions/hash"
8
+ require "api_ai_wrapper/constants"
9
+ require "api_ai_wrapper/errors"
10
+ require "api_ai_wrapper/engine"
@@ -0,0 +1,35 @@
1
+ module ApiAiWrapper::Components
2
+
3
+ class Component
4
+ attr_accessor :engine
5
+
6
+ # We define http_verb methods get/post/put/delete to add correct headers and parse result automatically
7
+ [:get, :post, :put, :delete].each do |http_verb|
8
+ define_method(http_verb.to_sym) do |url, params = {}|
9
+ raise_if_unauthorized # checks if the correct token is present
10
+ set_headers # sets headers with the correct token
11
+
12
+ response = self.engine.client.send(http_verb, url, params, self.engine.headers)
13
+
14
+ handle_response(JSON.parse(response.body))
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Throws an error if the call is not successful
21
+ # Returns response if no error is returned
22
+ def handle_response(response_body)
23
+ response = response_body.deep_symbolize_keys
24
+ response_status = response[:status]
25
+ response_code = response_status[:code]
26
+
27
+ if response_code != 200
28
+ raise ApiAiWrapper::Errors::Engine::ApiError.new(response_status[:errorDetails], response_code, response_status[:errorType])
29
+ end
30
+
31
+ response
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,16 @@
1
+ module ApiAiWrapper::Components
2
+ class ExtractorComponent < ApiAiWrapper::Components::Component
3
+
4
+ def raise_if_unauthorized
5
+ raise ApiAiWrapper::Errors::Engine::MissingToken.new("client token is missing") if self.engine.client_token.blank?
6
+ end
7
+
8
+ def set_headers
9
+ self.engine.headers = {
10
+ "Authorization" => "Bearer #{self.engine.client_token}",
11
+ "Content-Type" => "application/json; charset=utf-8"
12
+ }
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module ApiAiWrapper::Components
2
+ class TrainerComponent < ApiAiWrapper::Components::Component
3
+
4
+ def raise_if_unauthorized
5
+ raise ApiAiWrapper::Errors::Engine::MissingToken.new("developer token is missing") if self.engine.developer_token.blank?
6
+ end
7
+
8
+ def set_headers
9
+ self.engine.headers = {
10
+ "Authorization" => "Bearer #{self.engine.developer_token}",
11
+ "Content-Type" => "application/json; charset=utf-8"
12
+ }
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module ApiAiWrapper
2
+
3
+ class Constants
4
+ DEFAULT_BASE_URL = "https://api.api.ai/v1/"
5
+ DEFAULT_VERSION = "20150910"
6
+ DEFAULT_LOCALE = "en"
7
+ DEFAULT_CLIENT_TIMEOUT = 50000
8
+ end
9
+
10
+ end
@@ -0,0 +1,40 @@
1
+ require "api_ai_wrapper/components/component"
2
+ require "api_ai_wrapper/components/trainer_component"
3
+ require "api_ai_wrapper/components/extractor_component"
4
+ require "api_ai_wrapper/trainers/intent_trainer"
5
+ require "api_ai_wrapper/trainers/entity_trainer"
6
+ require "api_ai_wrapper/meaning_extractor"
7
+
8
+ module ApiAiWrapper
9
+ class Engine
10
+ AUTOLOAD_CLASSES = [
11
+ ApiAiWrapper::Trainers::EntityTrainer,
12
+ ApiAiWrapper::Trainers::IntentTrainer,
13
+ ApiAiWrapper::MeaningExtractor
14
+ ]
15
+
16
+ attr_accessor :client, :client_timeout, :headers, :locale, :base_url, :version, :client_token, :developer_token
17
+ attr_accessor :entity_trainer, :intent_trainer, :meaning_extractor
18
+
19
+ def initialize(options = {})
20
+ self.client = HTTPClient.new
21
+ self.client.receive_timeout = options[:client_timeout].presence || ApiAiWrapper::Constants::DEFAULT_CLIENT_TIMEOUT
22
+ self.locale = options[:locale].presence || ApiAiWrapper::Constants::DEFAULT_LOCALE
23
+ self.base_url = ApiAiWrapper::Constants::DEFAULT_BASE_URL
24
+ self.version = options[:version].presence || ApiAiWrapper::Constants::DEFAULT_VERSION
25
+ self.client_token = options[:client_token].presence
26
+ self.developer_token = options[:developer_token].presence
27
+
28
+ # RAISE ERROR IF NO TOKEN PRESENT
29
+ raise ApiAiWrapper::Errors::Engine::MissingTokens.new if self.client_token.blank? && self.developer_token.blank?
30
+
31
+ # define entity_trainer and intent_trainer on the fly
32
+ AUTOLOAD_CLASSES.each{ |class_name|
33
+ instance = class_name.new
34
+ instance.engine = self
35
+ self.send("#{class_name.to_s.demodulize.underscore}=", instance)
36
+ }
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ class ApiAiWrapper::Errors
2
+
3
+ module Request
4
+ class UnsupportedParams < StandardError; end
5
+ end
6
+
7
+ module Engine
8
+ class MissingTokens < StandardError
9
+ def initialize(_message = "You have not set a developer or client token for this engine")
10
+ @message = _message
11
+ end
12
+ end
13
+
14
+ class MissingToken < StandardError
15
+ def initialize(_message)
16
+ @message = "Unauthorized call - #{_message}"
17
+ @code = 401
18
+ end
19
+ end
20
+
21
+ class ApiError < StandardError
22
+ def initialize(_message, _code, _status)
23
+ @message = _message
24
+ @code = _code
25
+ @status = _status
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,37 @@
1
+ class Hash
2
+
3
+ def transform_keys
4
+ return enum_for(:transform_keys) unless block_given?
5
+ result = self.class.new
6
+ each_key do |key|
7
+ result[yield(key)] = self[key]
8
+ end
9
+ result
10
+ end
11
+
12
+ def _deep_transform_keys_in_object(object, &block)
13
+ case object
14
+ when Hash
15
+ object.each_with_object({}) do |(key, value), result|
16
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
17
+ end
18
+ when Array
19
+ object.map {|e| _deep_transform_keys_in_object(e, &block) }
20
+ else
21
+ object
22
+ end
23
+ end
24
+
25
+ def deep_transform_keys(&block)
26
+ _deep_transform_keys_in_object(self, &block)
27
+ end
28
+
29
+ def symbolize_keys
30
+ transform_keys { |key| key.to_sym rescue key }
31
+ end
32
+
33
+ def deep_symbolize_keys
34
+ deep_transform_keys { |key| key.to_sym rescue key }
35
+ end
36
+
37
+ end
@@ -0,0 +1,13 @@
1
+ class Object
2
+ def blank?
3
+ respond_to?(:empty?) ? empty? : !self
4
+ end
5
+
6
+ def present?
7
+ !blank?
8
+ end
9
+
10
+ def presence
11
+ self if present?
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ class String
2
+
3
+ def demodulize
4
+ if i = self.rindex("::")
5
+ self[(i + 2)..-1]
6
+ else
7
+ self
8
+ end
9
+ end
10
+
11
+ def underscore
12
+ return self unless /[A-Z-]|::/.match?(self)
13
+ word = self.gsub("::".freeze, "/".freeze)
14
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
15
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
16
+ word.tr!("-".freeze, "_".freeze)
17
+ word.downcase!
18
+ word
19
+ end
20
+
21
+ end
@@ -0,0 +1,28 @@
1
+ module ApiAiWrapper
2
+
3
+ # https://api.ai/docs/reference/agent/query#query_parameters_and_json_fields
4
+ class MeaningExtractor < ApiAiWrapper::Components::ExtractorComponent
5
+
6
+ # https://api.ai/docs/reference/agent/query#post_query
7
+ # Retrieves the meaning of a utterance
8
+ # options can contain (in accordance with API reference) :
9
+ # - contexts
10
+ # - location
11
+ # - timezone
12
+ # - lang
13
+ # - sessionId
14
+ def post_query(query, options = {})
15
+ set_headers
16
+ body = {
17
+ query: query,
18
+ lang: self.engine.locale,
19
+ sessionId: SecureRandom.hex
20
+ }.merge(options)
21
+ endpoint_url = URI.join(self.engine.base_url, "query?v=#{self.engine.version}")
22
+
23
+ res = self.post(endpoint_url, body.to_json)
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,83 @@
1
+ module ApiAiWrapper::Trainers
2
+
3
+ # https://api.ai/docs/reference/agent/entities#overview
4
+ # https://api.ai/docs/reference/agent/entities#entity_object
5
+ class EntityTrainer < ApiAiWrapper::Components::TrainerComponent
6
+
7
+ # https://api.ai/docs/reference/agent/entities#get_entities
8
+ # Retrieves all entities for a given token
9
+ def get_entities
10
+ endpoint_url = URI.join(self.engine.base_url, "entities?v=#{self.engine.version}")
11
+
12
+ self.get(endpoint_url, {})
13
+ end
14
+
15
+ # https://api.ai/docs/reference/agent/entities#get_entitieseid
16
+ # Retrieves entity info
17
+ # eid can either be the ID or the entity NAME
18
+ def get_entity(eid)
19
+ endpoint_url = URI.join(self.engine.base_url, "entities/#{eid}?v=#{self.engine.version}")
20
+
21
+ self.get(endpoint_url, {})
22
+ end
23
+
24
+ # https://docs.api.ai/docs/entities#post-entities
25
+ # Creates an entity with the corresponding entries
26
+ def post_entity(name, entries)
27
+ body = {
28
+ name: name,
29
+ entries: entries
30
+ }
31
+ endpoint_url = URI.join(self.engine.base_url, "entities?v=#{self.engine.version}")
32
+
33
+ self.post(endpoint_url, body.to_json)
34
+ end
35
+
36
+ # https://api.ai/docs/reference/agent/entities#post_entitieseidentries
37
+ # Allows to add entries to en existing entity
38
+ # eid can either be the ID or the entity NAME
39
+ def post_entity_entries(eid, entries)
40
+ endpoint_url = URI.join(self.engine.base_url, "entities/#{eid}/entries?v=#{self.engine.version}")
41
+
42
+ self.post(endpoint_url, entries.to_json)
43
+ end
44
+
45
+ # https://api.ai/docs/reference/agent/entities#put_entitieseid
46
+ # Update an entity
47
+ def put_entity(eid, options = {})
48
+ endpoint_url = URI.join(self.engine.base_url, "entities/#{eid}?v=#{self.engine.version}")
49
+
50
+ self.put(endpoint_url, options.to_json)
51
+ end
52
+
53
+ # https://api.ai/docs/reference/agent/entities#put_entitieseid
54
+ # Update an entity's entries
55
+ def put_entity_entries(eid, entries)
56
+ body = { entries: entries }
57
+ endpoint_url = URI.join(self.engine.base_url, "entities/#{eid}/entries?v=#{self.engine.version}")
58
+
59
+ self.put(endpoint_url, entries.to_json)
60
+ end
61
+
62
+ # https://api.ai/docs/reference/agent/entities#delete_entitieseid
63
+ # Delete an entity
64
+ # eid can either be the ID or the entity NAME
65
+ def delete_entity(eid)
66
+ endpoint_url = URI.join(self.engine.base_url, "entities/#{eid}?v=#{self.engine.version}")
67
+
68
+ self.delete(endpoint_url, {})
69
+ end
70
+
71
+ # https://api.ai/docs/reference/agent/entities#delete_entitieseidentries
72
+ # Delete an entity's entries
73
+ # eid can either be the ID or the entity NAME
74
+ # entries is an array of reference values (e.g. ["blue", "red"] for entity "color")
75
+ def delete_entity_entries(eid, entries)
76
+ body = { entries: entries }
77
+ endpoint_url = URI.join(self.engine.base_url, "entities/#{eid}/entries?v=#{self.engine.version}")
78
+
79
+ self.delete(endpoint_url, body.to_json)
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,72 @@
1
+ module ApiAiWrapper::Trainers
2
+
3
+ # https://api.ai/docs/reference/agent/intents#overview
4
+ # https://api.ai/docs/reference/agent/intents#intent_object
5
+ class IntentTrainer < ApiAiWrapper::Components::TrainerComponent
6
+
7
+ # https://api.ai/docs/reference/agent/intents#get_intents
8
+ # Fetches all intents for a given token
9
+ def get_intents
10
+ set_headers
11
+ endpoint_url = URI.join(self.engine.base_url, "intents?v=#{self.engine.version}")
12
+
13
+ res = self.engine.client.get(endpoint_url, {}, self.engine.headers)
14
+
15
+ JSON.parse(res.body)
16
+ end
17
+
18
+ # https://api.ai/docs/reference/agent/intents#get_intentsiid
19
+ # Retrieves intent info
20
+ def get_intent(iid)
21
+ set_headers
22
+ endpoint_url = URI.join(self.engine.base_url, "intents/#{iid}?v=#{self.engine.version}")
23
+
24
+ res = self.engine.client.get(endpoint_url, {}, self.engine.headers)
25
+
26
+ JSON.parse(res.body)
27
+ end
28
+
29
+ # https://api.ai/docs/reference/agent/intents#post_intents
30
+ # Creates an intent
31
+ # options can contain (in accordance with API reference) :
32
+ # - contexts
33
+ # - templates
34
+ # - responses
35
+ def post_intent(name, user_says_data, options = {})
36
+ set_headers
37
+ body = options.merge({
38
+ name: name,
39
+ auto: true, # ML activated
40
+ userSays: user_says_data
41
+ })
42
+ endpoint_url = URI.join(self.engine.base_url, "intents?v=#{self.engine.version}")
43
+
44
+ res = self.engine.client.post(endpoint_url, body.to_json, self.engine.headers)
45
+
46
+ JSON.parse(res.body)
47
+ end
48
+
49
+ # https://api.ai/docs/reference/agent/intents#put_intentsiid
50
+ # Update an intent
51
+ def put_intent(iid, options = {})
52
+ set_headers
53
+ endpoint_url = URI.join(self.engine.base_url, "intents/#{iid}?v=#{self.engine.version}")
54
+
55
+ res = self.engine.client.put(endpoint_url, options.to_json, self.engine.headers)
56
+
57
+ JSON.parse(res.body)
58
+ end
59
+
60
+ # https://api.ai/docs/reference/agent/entities#delete_entitieseid
61
+ # Delete an intent
62
+ def delete_intent(iid)
63
+ set_headers
64
+ endpoint_url = URI.join(self.engine.base_url, "intents/#{iid}?v=#{self.engine.version}")
65
+
66
+ res = self.engine.client.delete(endpoint_url, {}, self.engine.headers)
67
+
68
+ JSON.parse(res.body)
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,80 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ApiAiWrapper::Components::Component do
4
+
5
+ let :engine do
6
+ ApiAiWrapper::Engine.new({
7
+ client_token: "some-token",
8
+ developer_token: "some-token"
9
+ })
10
+ end
11
+
12
+ let :component do
13
+ comp = ApiAiWrapper::Components::Component.new
14
+ comp.engine = engine
15
+ comp
16
+ end
17
+
18
+ describe "http_verbs" do
19
+ it "should define http_verb methods get/post/put/delete" do
20
+ [:get, :post, :put, :delete].each do |http_verb|
21
+ expect(component.respond_to?(http_verb)).to be true
22
+ end
23
+ end
24
+
25
+ it "should call #raise_if_unauthorized for each http_verb methods" do
26
+ expect(engine.intent_trainer).to receive(:raise_if_unauthorized).exactly(4).times
27
+ allow(engine.intent_trainer).to receive(:set_headers).and_call_original
28
+ allow(engine.intent_trainer).to receive(:handle_response)
29
+
30
+ [:get, :post, :put, :delete].each do |http_verb|
31
+ stub_call(engine, "some-url", { method: http_verb })
32
+ engine.intent_trainer.send(http_verb, URI.join(ApiAiWrapper::Constants::DEFAULT_BASE_URL, "some-url"))
33
+ end
34
+ end
35
+
36
+ it "should call #set_headers for each http_verb methods" do
37
+ allow(engine.intent_trainer).to receive(:raise_if_unauthorized)
38
+ expect(engine.intent_trainer).to receive(:set_headers).exactly(4).times.and_call_original
39
+ allow(engine.intent_trainer).to receive(:handle_response)
40
+
41
+ [:get, :post, :put, :delete].each do |http_verb|
42
+ stub_call(engine, "some-url", { method: http_verb })
43
+ engine.intent_trainer.send(http_verb, URI.join(ApiAiWrapper::Constants::DEFAULT_BASE_URL, "some-url"))
44
+ end
45
+ end
46
+
47
+ it "should call #handle_response for each http_verb methods" do
48
+ allow(engine.intent_trainer).to receive(:raise_if_unauthorized)
49
+ allow(engine.intent_trainer).to receive(:set_headers).and_call_original
50
+ expect(engine.intent_trainer).to receive(:handle_response).exactly(4).times.and_call_original
51
+
52
+ [:get, :post, :put, :delete].each do |http_verb|
53
+ stub_call(engine, "some-url", { method: http_verb })
54
+ engine.intent_trainer.send(http_verb, URI.join(ApiAiWrapper::Constants::DEFAULT_BASE_URL, "some-url"))
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "#handle_response(response)" do
60
+ context "when API.AI call is successful (code is 200)" do
61
+ it "should return JSON parsed response" do
62
+ response = { "status" => { "code" => 200, "errorType" => "success" } }
63
+
64
+ expect(component.send(:handle_response, response)).to eq(response.deep_symbolize_keys)
65
+ end
66
+ end
67
+
68
+ context "when API.AI call is in error (code is not 200)" do
69
+ it "should return JSON parsed response" do
70
+ response = { "status" => { "code" => 203, "errorType" => "problem", "errorDetails" => "an error" } }
71
+
72
+ expect {
73
+ component.send(:handle_response, response)
74
+ }.to raise_error(ApiAiWrapper::Errors::Engine::ApiError)
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ApiAiWrapper::Components::ExtractorComponent do
4
+
5
+ describe "#raise_if_unauthorized" do
6
+ it "should not raise if call is client_token is present" do
7
+ engine = ApiAiWrapper::Engine.new(client_token: "some-token")
8
+ expect {
9
+ engine.meaning_extractor.raise_if_unauthorized
10
+ }.not_to raise_error
11
+ end
12
+
13
+ it "should raise if call is client_token is blank" do
14
+ engine = ApiAiWrapper::Engine.new(client_token: nil, developer_token: "some-token")
15
+ expect {
16
+ engine.meaning_extractor.raise_if_unauthorized
17
+ }.to raise_error(ApiAiWrapper::Errors::Engine::MissingToken)
18
+ end
19
+ end
20
+
21
+ describe "#set_headers" do
22
+ let :engine do
23
+ ApiAiWrapper::Engine.new({
24
+ client_token: "some-token-1",
25
+ developer_token: "some-token"
26
+ })
27
+ end
28
+
29
+ it "should set authorization and content type headers correctly" do
30
+ expect(engine.headers).to be_nil
31
+
32
+ engine.meaning_extractor.set_headers
33
+
34
+ expect(engine.headers).to eq({ "Authorization" => "Bearer some-token-1", "Content-Type" => "application/json; charset=utf-8" })
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ApiAiWrapper::Components::TrainerComponent do
4
+
5
+ describe "#raise_if_unauthorized" do
6
+ it "should not raise if call is developer_token is present" do
7
+ engine = ApiAiWrapper::Engine.new(developer_token: "some-token")
8
+ expect {
9
+ engine.entity_trainer.raise_if_unauthorized
10
+ }.not_to raise_error
11
+ end
12
+
13
+ it "should raise if call is developer_token is blank" do
14
+ engine = ApiAiWrapper::Engine.new(client_token: "some-token", developer_token: nil)
15
+ expect {
16
+ engine.entity_trainer.raise_if_unauthorized
17
+ }.to raise_error(ApiAiWrapper::Errors::Engine::MissingToken)
18
+ end
19
+ end
20
+
21
+ describe "#set_headers" do
22
+ let :engine do
23
+ ApiAiWrapper::Engine.new({
24
+ client_token: "some-token",
25
+ developer_token: "some-token-1"
26
+ })
27
+ end
28
+
29
+ it "should set authorization and content type headers correctly" do
30
+ expect(engine.headers).to be_nil
31
+
32
+ engine.intent_trainer.set_headers
33
+
34
+ expect(engine.headers).to eq({ "Authorization" => "Bearer some-token-1", "Content-Type" => "application/json; charset=utf-8" })
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,86 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ApiAiWrapper::Engine do
4
+
5
+ let :engine do
6
+ ApiAiWrapper::Engine.new({
7
+ client_token: "some-token",
8
+ developer_token: "some-token"
9
+ })
10
+ end
11
+
12
+ let :default_params do
13
+ { client_token: "some-token", developer_token: "some-token" }
14
+ end
15
+
16
+ describe "#initialize" do
17
+
18
+ it "should instantiate a HTTPClient as a 'client' accessor" do
19
+ expect(engine.client).to be_instance_of(HTTPClient)
20
+ end
21
+
22
+ describe "client_timeout" do
23
+ it "should instantiate a default 'client_timeout' accessor as DEFAULT_CLIENT_TIMEOUT" do
24
+ expect(engine.client.receive_timeout).to eq(ApiAiWrapper::Constants::DEFAULT_CLIENT_TIMEOUT)
25
+ end
26
+
27
+ it "should instantiate a 'client_timeout' accessor when specified in options" do
28
+ expect(ApiAiWrapper::Engine.new(default_params.merge({ client_timeout: 1000 })).client.receive_timeout).to eq(1000)
29
+ end
30
+ end
31
+
32
+ describe "locale" do
33
+ it "should instantiate a default 'locale' accessor as en" do
34
+ expect(engine.locale).to eq(ApiAiWrapper::Constants::DEFAULT_LOCALE)
35
+ end
36
+
37
+ it "should instantiate a 'locale' accessor when specified in options" do
38
+ expect(ApiAiWrapper::Engine.new(default_params.merge({ locale: "fr" })).locale).to eq("fr")
39
+ end
40
+ end
41
+
42
+ it "should instantiate the correct default base url as 'base_url' accessor" do
43
+ expect(engine.base_url).to eq(ApiAiWrapper::Constants::DEFAULT_BASE_URL)
44
+ end
45
+
46
+ describe "version" do
47
+ it "should instantiate a default 'version' accessor" do
48
+ expect(engine.version).to eq(ApiAiWrapper::Constants::DEFAULT_VERSION)
49
+ end
50
+
51
+ it "should instantiate a 'locale' accessor when specified in options" do
52
+ expect(ApiAiWrapper::Engine.new(default_params.merge({ version: "some-version" })).version).to eq("some-version")
53
+ end
54
+ end
55
+
56
+ describe "tokens" do
57
+ it "should instantiate a 'client_token' accessor when specified in options" do
58
+ expect(ApiAiWrapper::Engine.new({ client_token: "some-token" }).client_token).to eq("some-token")
59
+ end
60
+
61
+ it "should instantiate a 'developer_token' accessor when specified in options" do
62
+ expect(ApiAiWrapper::Engine.new({ developer_token: "some-token" }).developer_token).to eq("some-token")
63
+ end
64
+
65
+ it "should raise MissingToken error if no token is present" do
66
+ expect { ApiAiWrapper::Engine.new }.to raise_error(ApiAiWrapper::Errors::Engine::MissingTokens)
67
+ end
68
+ end
69
+
70
+ describe "class autoloading" do
71
+ it "should instantiate an EntityTrainer as 'entity_trainer' accessor" do
72
+ expect(engine.entity_trainer).to be_instance_of(ApiAiWrapper::Trainers::EntityTrainer)
73
+ end
74
+
75
+ it "should instantiate an IntentTrainer as 'intent_trainer' accessor" do
76
+ expect(engine.intent_trainer).to be_instance_of(ApiAiWrapper::Trainers::IntentTrainer)
77
+ end
78
+
79
+ it "should instantiate an MeaningExtractor as 'meaning_extractor' accessor" do
80
+ expect(engine.meaning_extractor).to be_instance_of(ApiAiWrapper::MeaningExtractor)
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,30 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ApiAiWrapper::MeaningExtractor do
4
+
5
+ let :engine do
6
+ ApiAiWrapper::Engine.new({
7
+ client_token: "some-token",
8
+ developer_token: "some-token"
9
+ })
10
+ end
11
+
12
+ let :meaning_extractor do
13
+ engine.meaning_extractor
14
+ end
15
+
16
+ describe "#post_query(query, options = {})" do
17
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
18
+ stub_call(engine, "query?v=#{engine.version}", {
19
+ method: :post,
20
+ request_body: {
21
+ query: "some-query",
22
+ lang: engine.locale,
23
+ sessionId: "some-session-id"
24
+ }.to_json
25
+ })
26
+ meaning_extractor.post_query("some-query", { sessionId: "some-session-id" })
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,98 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ApiAiWrapper::Trainers::EntityTrainer do
4
+
5
+ let :engine do
6
+ ApiAiWrapper::Engine.new({
7
+ client_token: "some-token",
8
+ developer_token: "some-token"
9
+ })
10
+ end
11
+
12
+ let :entity_trainer do
13
+ engine.entity_trainer
14
+ end
15
+
16
+ describe "#get_entities" do
17
+ it "should send a call to the correct endpoint with appropriate headers" do
18
+ stub_call(engine, "entities")
19
+ entity_trainer.get_entities
20
+ end
21
+ end
22
+
23
+ describe "#get_entity(eid)" do
24
+ it "should send a call to the correct endpoint with appropriate headers" do
25
+ stub_call(engine, "entities/some-id")
26
+ entity_trainer.get_entity("some-id")
27
+ end
28
+ end
29
+
30
+ describe "#post_entity(name, entries)" do
31
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
32
+ stub_call(engine, "entities?v=#{engine.version}", {
33
+ method: :post,
34
+ request_body: {
35
+ name: "some-name",
36
+ entries: "some-entries"
37
+ }.to_json
38
+ })
39
+ entity_trainer.post_entity("some-name", "some-entries")
40
+ end
41
+ end
42
+
43
+ describe "#post_entity_entries(eid, entries)" do
44
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
45
+ stub_call(engine, "entities/some-id/entries?v=#{engine.version}", {
46
+ method: :post,
47
+ request_body: "some-entries".to_json
48
+ })
49
+ entity_trainer.post_entity_entries("some-id", "some-entries")
50
+ end
51
+ end
52
+
53
+ describe "#put_entity(eid, options = {})" do
54
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
55
+ stub_call(engine, "entities/some-id?v=#{engine.version}", {
56
+ method: :put,
57
+ request_body: {
58
+ name: "new-name"
59
+ }.to_json
60
+ })
61
+ entity_trainer.put_entity("some-id", { name: "new-name" })
62
+ end
63
+ end
64
+
65
+ describe "#put_entity_entries(eid, entries)" do
66
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
67
+ stub_call(engine, "entities/some-id/entries?v=#{engine.version}", {
68
+ method: :put,
69
+ request_body: {
70
+ entries: "new-entries"
71
+ }.to_json
72
+ })
73
+ entity_trainer.put_entity_entries("some-id", { entries: "new-entries" })
74
+ end
75
+ end
76
+
77
+ describe "#delete_entity(eid)" do
78
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
79
+ stub_call(engine, "entities/some-id?v=#{engine.version}", {
80
+ method: :delete
81
+ })
82
+ entity_trainer.delete_entity("some-id")
83
+ end
84
+ end
85
+
86
+ describe "#delete_entity_entries(eid, entries)" do
87
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
88
+ stub_call(engine, "entities/some-id/entries?v=#{engine.version}", {
89
+ method: :delete,
90
+ request_body: {
91
+ entries: "entries-to-delete"
92
+ }
93
+ })
94
+ entity_trainer.delete_entity_entries("some-id", "entries-to-delete")
95
+ end
96
+ end
97
+
98
+ end
@@ -0,0 +1,65 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe ApiAiWrapper::Trainers::IntentTrainer do
4
+
5
+ let :engine do
6
+ ApiAiWrapper::Engine.new({
7
+ client_token: "some-token",
8
+ developer_token: "some-token"
9
+ })
10
+ end
11
+
12
+ let :intent_trainer do
13
+ engine.intent_trainer
14
+ end
15
+
16
+ describe "#get_intents" do
17
+ it "should send a call to the correct endpoint with appropriate headers" do
18
+ stub_call(engine, "intents")
19
+ intent_trainer.get_intents
20
+ end
21
+ end
22
+
23
+ describe "#get_intent(iid)" do
24
+ it "should send a call to the correct endpoint with appropriate headers" do
25
+ stub_call(engine, "intents/some-id")
26
+ intent_trainer.get_intent("some-id")
27
+ end
28
+ end
29
+
30
+ describe "#post_intent(name, user_says_data, options = {})" do
31
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
32
+ stub_call(engine, "intents?v=#{engine.version}", {
33
+ method: :post,
34
+ request_body: {
35
+ name: "some-name",
36
+ auto: true,
37
+ userSays: "some-data"
38
+ }.to_json
39
+ })
40
+ intent_trainer.post_intent("some-name", "some-data")
41
+ end
42
+ end
43
+
44
+ describe "#put_intent(iid, options = {})" do
45
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
46
+ stub_call(engine, "intents/some-id?v=#{engine.version}", {
47
+ method: :put,
48
+ request_body: {
49
+ name: "new-name"
50
+ }.to_json
51
+ })
52
+ intent_trainer.put_intent("some-id", { name: "new-name" })
53
+ end
54
+ end
55
+
56
+ describe "#delete_intent(iid)" do
57
+ it "should send a call to the correct endpoint with appropriate headers and body params" do
58
+ stub_call(engine, "intents/some-id?v=#{engine.version}", {
59
+ method: :delete
60
+ })
61
+ intent_trainer.delete_intent("some-id")
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,111 @@
1
+ # This file was generated by the `rails generate rspec:install` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # The `.rspec` file also contains a few flags that are not defaults but that
16
+ # users commonly want.
17
+ #
18
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19
+ RSpec.configure do |config|
20
+ # rspec-expectations config goes here. You can use an alternate
21
+ # assertion/expectation library such as wrong or the stdlib/minitest
22
+ # assertions if you prefer.
23
+ config.expect_with :rspec do |expectations|
24
+ # This option will default to `true` in RSpec 4. It makes the `description`
25
+ # and `failure_message` of custom matchers include text for helper methods
26
+ # defined using `chain`, e.g.:
27
+ # be_bigger_than(2).and_smaller_than(4).description
28
+ # # => "be bigger than 2 and smaller than 4"
29
+ # ...rather than:
30
+ # # => "be bigger than 2"
31
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
32
+ end
33
+
34
+ # rspec-mocks config goes here. You can use an alternate test double
35
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
36
+ config.mock_with :rspec do |mocks|
37
+ # Prevents you from mocking or stubbing a method that does not exist on
38
+ # a real object. This is generally recommended, and will default to
39
+ # `true` in RSpec 4.
40
+ mocks.verify_partial_doubles = true
41
+ end
42
+
43
+ # These two settings work together to allow you to limit a spec run
44
+ # to individual examples or groups you care about by tagging them with
45
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
46
+ # get run.
47
+ config.filter_run :focus
48
+ config.run_all_when_everything_filtered = true
49
+
50
+ # Allows RSpec to persist some state between runs in order to support
51
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
52
+ # you configure your source control system to ignore this file.
53
+ config.example_status_persistence_file_path = "spec/examples.txt"
54
+
55
+ # Limits the available syntax to the non-monkey patched syntax that is
56
+ # recommended. For more details, see:
57
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
58
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
59
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
60
+ config.disable_monkey_patching!
61
+
62
+ # Many RSpec users commonly either run the entire suite or an individual
63
+ # file, and it's useful to allow more verbose output when running an
64
+ # individual spec file.
65
+ if config.files_to_run.one?
66
+ # Use the documentation formatter for detailed output,
67
+ # unless a formatter has already been configured
68
+ # (e.g. via a command-line flag).
69
+ config.default_formatter = 'doc'
70
+ end
71
+
72
+ # Print the 10 slowest examples and example groups at the
73
+ # end of the spec run, to help surface which specs are running
74
+ # particularly slow.
75
+ # config.profile_examples = 10
76
+
77
+ # Run specs in random order to surface order dependencies. If you find an
78
+ # order dependency and want to debug it, you can fix the order by providing
79
+ # the seed, which is printed after each run.
80
+ # --seed 1234
81
+ config.order = :random
82
+
83
+ # Seed global randomization in this process using the `--seed` CLI option.
84
+ # Setting this allows you to use `--seed` to deterministically reproduce
85
+ # test failures related to randomization by passing the same `--seed` value
86
+ # as the one that triggered the failure.
87
+ Kernel.srand config.seed
88
+
89
+ config.color = true
90
+ end
91
+
92
+ require "simplecov"
93
+ SimpleCov.start "rails"
94
+
95
+ require "webmock/rspec"
96
+ WebMock.disable_net_connect!
97
+
98
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'api_ai_wrapper')
99
+
100
+ def stub_call(engine, path, options = {})
101
+ stub_request(options[:method].presence || :get, URI.join(engine.base_url, path))
102
+ .with({
103
+ headers: { "Authorization" => "Bearer some-token", "Content-Type" => "application/json; charset=utf-8" },
104
+ body: options[:request_body] || nil
105
+ })
106
+ .to_return(body: (options[:return_body].presence || { "status" => { "code" => 200, "errorType" => "success" } }.to_json))
107
+ end
108
+
109
+ def fixture_path
110
+ File.join(File.dirname(__FILE__), '..', 'spec', 'fixtures')
111
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api_ai_wrapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Vincent Gabou
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httpclient
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.8.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.8.0
33
+ description: A simple ruby library that let's any developer automate the training
34
+ process of a Natural Language Processing Engine on API.AI, and retrieve meaning
35
+ from new utterances.
36
+ email: vincent.gabou@gmail.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - ".gitignore"
42
+ - Gemfile
43
+ - Gemfile.lock
44
+ - LICENSE
45
+ - README.md
46
+ - api_ai_wrapper.gemspec
47
+ - lib/api_ai_wrapper.rb
48
+ - lib/api_ai_wrapper/components/component.rb
49
+ - lib/api_ai_wrapper/components/extractor_component.rb
50
+ - lib/api_ai_wrapper/components/trainer_component.rb
51
+ - lib/api_ai_wrapper/constants.rb
52
+ - lib/api_ai_wrapper/engine.rb
53
+ - lib/api_ai_wrapper/errors.rb
54
+ - lib/api_ai_wrapper/extensions/hash.rb
55
+ - lib/api_ai_wrapper/extensions/object.rb
56
+ - lib/api_ai_wrapper/extensions/string.rb
57
+ - lib/api_ai_wrapper/meaning_extractor.rb
58
+ - lib/api_ai_wrapper/trainers/entity_trainer.rb
59
+ - lib/api_ai_wrapper/trainers/intent_trainer.rb
60
+ - spec/api_ai_wrapper/components/component_spec.rb
61
+ - spec/api_ai_wrapper/components/extractor_component_spec.rb
62
+ - spec/api_ai_wrapper/components/trainer_component_spec.rb
63
+ - spec/api_ai_wrapper/engine_spec.rb
64
+ - spec/api_ai_wrapper/meaning_extractor_spec.rb
65
+ - spec/api_ai_wrapper/trainers/entity_trainer_spec.rb
66
+ - spec/api_ai_wrapper/trainers/intent_trainer_spec.rb
67
+ - spec/spec_helper.rb
68
+ homepage: http://rubygems.org/gems/api_ai_wrapper
69
+ licenses:
70
+ - MIT
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 2.6.11
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: An API.AI Ruby Wrapper
92
+ test_files: []