api_ai_wrapper 1.0.0

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.
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: []