alexa_skills_ruby 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +21 -0
  5. data/README.md +58 -0
  6. data/Rakefile +7 -0
  7. data/alexa_skills_ruby.gemspec +26 -0
  8. data/lib/alexa_skills_ruby.rb +27 -0
  9. data/lib/alexa_skills_ruby/errors.rb +4 -0
  10. data/lib/alexa_skills_ruby/handler.rb +87 -0
  11. data/lib/alexa_skills_ruby/json_object.rb +122 -0
  12. data/lib/alexa_skills_ruby/json_objects/application.rb +7 -0
  13. data/lib/alexa_skills_ruby/json_objects/base_request.rb +40 -0
  14. data/lib/alexa_skills_ruby/json_objects/card.rb +16 -0
  15. data/lib/alexa_skills_ruby/json_objects/image.rb +7 -0
  16. data/lib/alexa_skills_ruby/json_objects/intent.rb +11 -0
  17. data/lib/alexa_skills_ruby/json_objects/intent_request.rb +7 -0
  18. data/lib/alexa_skills_ruby/json_objects/launch_request.rb +7 -0
  19. data/lib/alexa_skills_ruby/json_objects/output_speech.rb +21 -0
  20. data/lib/alexa_skills_ruby/json_objects/reprompt.rb +7 -0
  21. data/lib/alexa_skills_ruby/json_objects/response.rb +29 -0
  22. data/lib/alexa_skills_ruby/json_objects/session.rb +9 -0
  23. data/lib/alexa_skills_ruby/json_objects/session_ended_request.rb +7 -0
  24. data/lib/alexa_skills_ruby/json_objects/skills_request.rb +9 -0
  25. data/lib/alexa_skills_ruby/json_objects/skills_response.rb +14 -0
  26. data/lib/alexa_skills_ruby/json_objects/user.rb +7 -0
  27. data/lib/alexa_skills_ruby/version.rb +3 -0
  28. data/spec/fixtures/example_intent.json +34 -0
  29. data/spec/fixtures/example_launch.json +19 -0
  30. data/spec/fixtures/example_response.json +28 -0
  31. data/spec/fixtures/example_session_ended.json +26 -0
  32. data/spec/spec_helper.rb +15 -0
  33. data/spec/support/fixture_support.rb +11 -0
  34. data/spec/unit/handler_spec.rb +142 -0
  35. data/spec/unit/json_object_spec.rb +59 -0
  36. data/spec/unit/json_objects/skills_request_spec.rb +56 -0
  37. data/spec/unit/json_objects/skills_response_spec.rb +48 -0
  38. metadata +173 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1101b97d3f88c45eb43d643d75e3e8e8ca6b26c6
4
+ data.tar.gz: 0b6350b302c7ed1b643471986233069fe0a139c7
5
+ SHA512:
6
+ metadata.gz: fc1a9404b923d9575c8a75aceed09e7bd0b9dda09230f08d475b199c7f7d53923160c39b9093d3d645872e8d0611eb3e805d31f40c35c11e56683a30aa9e2bdf
7
+ data.tar.gz: e3f2725899cd0147118eb291c6ba8f6753bb521d67232722695eaa78e9cd0eb6c58e9f4c1d12254da9d8796d7d36abe0a52a941a896cc88357f976a5f5869bbf
data/.gitignore ADDED
@@ -0,0 +1,36 @@
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
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalization:
25
+ /.bundle/
26
+ /vendor/bundle
27
+ /lib/bundler/man/
28
+
29
+ # for a library or gem, you might want to ignore these files since the code is
30
+ # intended to run in multiple environments; otherwise, check them in:
31
+ Gemfile.lock
32
+ # .ruby-version
33
+ # .ruby-gemset
34
+
35
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
36
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Dan Elbert
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,58 @@
1
+ # alexa-skills-ruby
2
+ Simple library to interface with the Alexa Skills Kit
3
+
4
+ The primary way to interact with this library is by extending the `AlexaSkillsRuby::Handler` class. Create a subclass and
5
+ register event handlers.
6
+
7
+ The following handlers are available:
8
+
9
+ * `on_authenticate` - called before checking the ApplicationID
10
+ * `on_session_start` - called first if the request is flagged as a new session
11
+ * `on_launch` - called for a LaunchRequest
12
+ * `on_session_end` - called for a SessionEndedRequest
13
+ * `on_intent` called for an IntentRequest. Takes an optional string argument that specifies which intents it will be trigged on
14
+
15
+ In event handlers, the following methods are available:
16
+
17
+ * `request` - returns the current Request object, a `AlexaSkillsRuby::JsonObjects::BaseRequest`
18
+ * `session` - returns the current Session object, a `AlexaSkillsRuby::JsonObjects::Session`
19
+ * `session_attributes` - Hash containing the current session attributes. If the response is not flagged to end the session, all values will be sent with the response
20
+ * `response` - returns the current Response object, a `AlexaSkillsRuby::JsonObjects::Response`
21
+ * `application_id` - returns the value specified in the constructor options
22
+ * `logger` - returns the value specified in the constructor options
23
+
24
+ The `AlexaSkillsRuby::Handler` constructor takes an options hash and processes the following keys:
25
+ * `application_id` - If set, will raise a `AlexaSkillsRuby::InvalidApplicationId` if a request's application_id does not match
26
+ * `logger` - Will be available through the `logger` method in the handler; not otherwise used by the base class
27
+
28
+ ## Example Sinatra App Using this Library
29
+
30
+ ```ruby
31
+ require 'sinatra'
32
+ require 'alexa_skills_ruby'
33
+
34
+ class CustomHandler < AlexaSkillsRuby::Handler
35
+
36
+ on_intent("GetZodiacHoroscopeIntent") do
37
+ slots = request.intent.slots
38
+ response.set_output_speech_text("Horiscope Text")
39
+ response.set_simple_card("title", "content")
40
+ logger.info 'GetZodiacHoroscopeIntent processed'
41
+ end
42
+
43
+ end
44
+
45
+ post '/' do
46
+ content_type :json
47
+
48
+ handler = CustomHandler.new(application_id: ENV['APPLICATION_ID'], logger: logger)
49
+
50
+ begin
51
+ handler.handle(request.body.read)
52
+ rescue AlexaSkillsRuby::InvalidApplicationId => e
53
+ logger.error e.to_s
54
+ 403
55
+ end
56
+
57
+ end
58
+ ```
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :test => :spec
7
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'alexa_skills_ruby/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "alexa_skills_ruby"
8
+ spec.version = AlexaSkillsRuby::VERSION
9
+ spec.authors = ["Dan Elbert"]
10
+ spec.email = ["dan.elbert@gmail.com"]
11
+ spec.homepage = 'https://github.com/DanElbert/alexa_skills_ruby'
12
+ spec.summary = %q{Simple library to interface with the Alexa Skills Kit}
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_runtime_dependency 'activesupport', '~> 4.2'
20
+ spec.add_runtime_dependency "multi_json", "~> 1.0"
21
+
22
+ spec.add_development_dependency "bundler"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec", "~> 3.4.0"
25
+ spec.add_development_dependency "oj", "~> 2.10.2"
26
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/callbacks'
3
+ require 'multi_json'
4
+
5
+ require 'alexa_skills_ruby/version'
6
+ require 'alexa_skills_ruby/json_object'
7
+ require 'alexa_skills_ruby/json_objects/application'
8
+ require 'alexa_skills_ruby/json_objects/user'
9
+ require 'alexa_skills_ruby/json_objects/session'
10
+ require 'alexa_skills_ruby/json_objects/intent'
11
+ require 'alexa_skills_ruby/json_objects/base_request'
12
+ require 'alexa_skills_ruby/json_objects/intent_request'
13
+ require 'alexa_skills_ruby/json_objects/launch_request'
14
+ require 'alexa_skills_ruby/json_objects/session_ended_request'
15
+ require 'alexa_skills_ruby/json_objects/image'
16
+ require 'alexa_skills_ruby/json_objects/card'
17
+ require 'alexa_skills_ruby/json_objects/output_speech'
18
+ require 'alexa_skills_ruby/json_objects/reprompt'
19
+ require 'alexa_skills_ruby/json_objects/response'
20
+ require 'alexa_skills_ruby/json_objects/skills_request'
21
+ require 'alexa_skills_ruby/json_objects/skills_response'
22
+ require 'alexa_skills_ruby/errors'
23
+ require 'alexa_skills_ruby/handler'
24
+
25
+ module AlexaSkillsRuby
26
+
27
+ end
@@ -0,0 +1,4 @@
1
+ module AlexaSkillsRuby
2
+ class InvalidApplicationId < StandardError
3
+ end
4
+ end
@@ -0,0 +1,87 @@
1
+ module AlexaSkillsRuby
2
+ class Handler
3
+ include ActiveSupport::Callbacks
4
+ define_callbacks :authenticate, :session_start, :launch, :intent, :session_end
5
+
6
+ attr_reader :request, :session, :response
7
+ attr_accessor :application_id, :logger
8
+
9
+ def initialize(opts = {})
10
+ if opts[:application_id]
11
+ @application_id = opts[:application_id]
12
+ end
13
+
14
+ if opts[:logger]
15
+ @logger = opts[:logger]
16
+ end
17
+ end
18
+
19
+ def session_attributes
20
+ @session.attributes ||= {}
21
+ end
22
+
23
+ def handle(request_json)
24
+ @skill_request = JsonObjects::SkillsRequest.new(MultiJson.load(request_json))
25
+ @skill_response = JsonObjects::SkillsResponse.new
26
+
27
+ @session = @skill_request.session
28
+ @request = @skill_request.request
29
+ @response = @skill_response.response
30
+
31
+
32
+ run_callbacks :authenticate do
33
+ if @application_id
34
+ if @application_id != session.application.application_id
35
+ raise InvalidApplicationId, "Invalid: [#{session.application.application_id}]"
36
+ end
37
+ end
38
+ end
39
+
40
+ if session.new
41
+ run_callbacks :session_start
42
+ end
43
+
44
+ case request
45
+ when JsonObjects::LaunchRequest
46
+ run_callbacks :launch
47
+ when JsonObjects::IntentRequest
48
+ run_callbacks :intent
49
+ when JsonObjects::SessionEndedRequest
50
+ run_callbacks :session_end
51
+ end
52
+
53
+ if response.should_end_session
54
+ @skill_response.session_attributes = {}
55
+ else
56
+ @skill_response.session_attributes = session_attributes
57
+ end
58
+
59
+ MultiJson.dump(@skill_response.as_json)
60
+ end
61
+
62
+ def self.on_authenticate(&block)
63
+ set_callback :authenticate, :before, block
64
+ end
65
+
66
+ def self.on_session_start(&block)
67
+ set_callback :session_start, :before, block
68
+ end
69
+
70
+ def self.on_launch(&block)
71
+ set_callback :launch, :before, block
72
+ end
73
+
74
+ def self.on_session_end(&block)
75
+ set_callback :session_end, :before, block
76
+ end
77
+
78
+ def self.on_intent(intent_name = nil, &block)
79
+ opts = {}
80
+ if intent_name
81
+ opts[:if] = -> { request.intent_name == intent_name }
82
+ end
83
+ set_callback :intent, :before, block, opts
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,122 @@
1
+ module AlexaSkillsRuby
2
+ class JsonObject
3
+
4
+ class_attribute :_json_object_properties
5
+ class_attribute :_properties
6
+ self._json_object_properties = {}
7
+ self._properties = []
8
+
9
+ def self.json_object_attribute(*args)
10
+ klass = args.pop
11
+ raise "Invalid arguments" unless klass && klass.is_a?(Class) && args.length > 0
12
+
13
+ args.compact.map { |a| a.to_sym }.each do |a|
14
+ attr_accessor a
15
+ self._json_object_properties[a.to_sym] = klass
16
+ end
17
+ end
18
+
19
+ def self.attribute(*attrs)
20
+ attrs.compact.map { |a| a.to_sym }.each do |a|
21
+ attr_accessor a
22
+ self._properties << a
23
+ end
24
+ end
25
+
26
+ class << self
27
+ alias_method :json_object_attributes, :json_object_attribute
28
+ alias_method :attributes, :attribute
29
+ end
30
+
31
+ # Copy properties on inheritance.
32
+ def self.inherited(subclass)
33
+ entities = _json_object_properties.dup
34
+ attrs = _properties.dup
35
+ subclass._json_object_properties = entities.each { |k, v| entities[k] = v.dup }
36
+ subclass._properties = attrs
37
+ super
38
+ end
39
+
40
+ def initialize(attrs = {})
41
+ populate_from_json(attrs)
42
+ end
43
+
44
+ def as_json(options = nil)
45
+ serialize_attributes(_properties + _json_object_properties.keys)
46
+ end
47
+
48
+ def to_json(options = nil)
49
+ MultiJson.dump(as_json(options))
50
+ end
51
+
52
+ def populate_from_json(attrs)
53
+ return unless attrs
54
+
55
+ attrs.each do |k, v|
56
+ meth = "#{get_method_name(k)}=".to_sym
57
+ if self.respond_to? meth
58
+ if json_object_class = _json_object_properties[k.to_sym]
59
+ assign_json_object(meth, v, json_object_class)
60
+ else
61
+ self.send(meth, v)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def serialize_attributes(attrs)
68
+ json = {}
69
+ attrs.each do |k|
70
+ meth = k.to_sym
71
+ if self.respond_to?(meth)
72
+ if _json_object_properties[k.to_sym]
73
+ val = serialize_json_object(meth)
74
+ else
75
+ val = self.send(meth)
76
+ end
77
+ unless val.nil?
78
+ json[get_attribute_name(k)] = val
79
+ end
80
+ end
81
+ end
82
+ json
83
+ end
84
+
85
+ protected
86
+
87
+ def assign_json_object(assignment_method, value, klass)
88
+
89
+ if value.is_a? Array
90
+ data = value.map { |v| hydrate_entity(v, klass) }
91
+ else
92
+ data = hydrate_entity(value, klass)
93
+ end
94
+
95
+ self.send(assignment_method, data)
96
+ end
97
+
98
+ def hydrate_entity(json, klass)
99
+ klass.new(json)
100
+ end
101
+
102
+ def get_method_name(attr_name)
103
+ attr_name.to_s.gsub(/([a-z])([A-Z])([a-z]|^)/) { |m| "#{m[0]}_#{m[1].downcase}#{m[2]}" }
104
+ end
105
+
106
+ def get_attribute_name(meth_name)
107
+ meth_name.to_s.gsub(/([a-z])(_)([a-z])/) { |m| m[0] + m[2].upcase }
108
+ end
109
+
110
+ def serialize_json_object(method)
111
+ value = self.send(method)
112
+ case value
113
+ when nil
114
+ nil
115
+ when Array
116
+ value.map { |v| v.as_json }
117
+ else
118
+ value.as_json
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,7 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class Application < JsonObject
4
+ attributes :application_id
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class BaseRequest < JsonObject
4
+ attributes :type, :request_id, :timestamp
5
+
6
+ def self.new(*args, &block)
7
+ json = args.first
8
+ subclass = case
9
+ when self != BaseRequest
10
+ nil
11
+ when json.nil?
12
+ nil
13
+ when json['type'] == 'LaunchRequest'
14
+ LaunchRequest
15
+ when json['type'] == 'IntentRequest'
16
+ IntentRequest
17
+ when json['type'] == 'SessionEndedRequest'
18
+ SessionEndedRequest
19
+ else
20
+ nil
21
+ end
22
+
23
+ if subclass
24
+ subclass.new(*args, &block)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def intent_name
31
+ if self.is_a? IntentRequest
32
+ self.intent.name
33
+ else
34
+ nil
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class Card < JsonObject
4
+ attributes :type, :title, :content, :text
5
+ json_object_attributes :image, Image
6
+
7
+ def self.simple(title, content)
8
+ card = new
9
+ card.type = "Simple"
10
+ card.title = title
11
+ card.content = content
12
+ card
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class Image < JsonObject
4
+ attributes :small_image_url, :large_image_url
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class Intent < JsonObject
4
+ attributes :name, :slots
5
+
6
+ def slots=(val)
7
+ @slots = Hash[(val || {}).map { |k, v| [k, v['value']] }]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class IntentRequest < BaseRequest
4
+ json_object_attribute :intent, Intent
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class LaunchRequest < BaseRequest
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class OutputSpeech < JsonObject
4
+ attributes :type, :text, :ssml
5
+
6
+ def self.text(text)
7
+ os = new
8
+ os.text = text
9
+ os.type = 'PlainText'
10
+ os
11
+ end
12
+
13
+ def self.ssml(ssml)
14
+ os = new
15
+ os.ssml = ssml
16
+ os.type = 'SSML'
17
+ os
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class Reprompt < JsonObject
4
+ json_object_attributes :output_speech, OutputSpeech
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class Response < JsonObject
4
+ attributes :should_end_session
5
+ json_object_attribute :output_speech, OutputSpeech
6
+ json_object_attribute :card, Card
7
+ json_object_attribute :reprompt, Reprompt
8
+
9
+ def initialize
10
+ self.should_end_session = true
11
+ end
12
+
13
+ def set_output_speech_text(text)
14
+ self.output_speech = OutputSpeech.text(text)
15
+ end
16
+
17
+ def set_simple_card(title, content)
18
+ self.card = Card.simple(title, content)
19
+ end
20
+
21
+ def set_reprompt_speech_text(text)
22
+ os = OutputSpeech.text(text)
23
+ rp = Reprompt.new
24
+ rp.output_speech = os
25
+ self.reprompt = rp
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class Session < JsonObject
4
+ attributes :new, :session_id, :attributes
5
+ json_object_attribute :application, Application
6
+ json_object_attribute :user, User
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class SessionEndedRequest < BaseRequest
4
+ attribute :reason
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class SkillsRequest < JsonObject
4
+ attribute :version
5
+ json_object_attribute :session, Session
6
+ json_object_attribute :request, BaseRequest
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class SkillsResponse < JsonObject
4
+ attributes :version, :session_attributes
5
+ json_object_attribute :response, Response
6
+
7
+ def initialize
8
+ self.version = '1.0'
9
+ self.session_attributes = {}
10
+ self.response = JsonObjects::Response.new
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module AlexaSkillsRuby
2
+ module JsonObjects
3
+ class User < JsonObject
4
+ attributes :user_id, :access_token
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module AlexaSkillsRuby
2
+ VERSION = '0.0.5'
3
+ end
@@ -0,0 +1,34 @@
1
+ {
2
+ "version": "1.0",
3
+ "session": {
4
+ "new": false,
5
+ "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000",
6
+ "application": {
7
+ "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
8
+ },
9
+ "attributes": {
10
+ "supportedHoroscopePeriods": {
11
+ "daily": true,
12
+ "weekly": false,
13
+ "monthly": false
14
+ }
15
+ },
16
+ "user": {
17
+ "userId": "amzn1.account.AM3B00000000000000000000000"
18
+ }
19
+ },
20
+ "request": {
21
+ "type": "IntentRequest",
22
+ "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000",
23
+ "timestamp": "2015-05-13T12:34:56Z",
24
+ "intent": {
25
+ "name": "GetZodiacHoroscopeIntent",
26
+ "slots": {
27
+ "ZodiacSign": {
28
+ "name": "ZodiacSign",
29
+ "value": "virgo"
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": "1.0",
3
+ "session": {
4
+ "new": true,
5
+ "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000",
6
+ "application": {
7
+ "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
8
+ },
9
+ "attributes": {},
10
+ "user": {
11
+ "userId": "amzn1.account.AM3B00000000000000000000000"
12
+ }
13
+ },
14
+ "request": {
15
+ "type": "LaunchRequest",
16
+ "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000",
17
+ "timestamp": "2015-05-13T12:34:56Z"
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "version": "1.0",
3
+ "sessionAttributes": {
4
+ "supportedHoriscopePeriods": {
5
+ "daily": true,
6
+ "weekly": false,
7
+ "monthly": false
8
+ }
9
+ },
10
+ "response": {
11
+ "outputSpeech": {
12
+ "type": "PlainText",
13
+ "text": "Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless. Can I help you with anything else?"
14
+ },
15
+ "card": {
16
+ "type": "Simple",
17
+ "title": "Horoscope",
18
+ "content": "Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless."
19
+ },
20
+ "reprompt": {
21
+ "outputSpeech": {
22
+ "type": "PlainText",
23
+ "text": "Can I help you with anything else?"
24
+ }
25
+ },
26
+ "shouldEndSession": false
27
+ }
28
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": "1.0",
3
+ "session": {
4
+ "new": false,
5
+ "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000",
6
+ "application": {
7
+ "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
8
+ },
9
+ "attributes": {
10
+ "supportedHoroscopePeriods": {
11
+ "daily": true,
12
+ "weekly": false,
13
+ "monthly": false
14
+ }
15
+ },
16
+ "user": {
17
+ "userId": "amzn1.account.AM3B00000000000000000000000"
18
+ }
19
+ },
20
+ "request": {
21
+ "type": "SessionEndedRequest",
22
+ "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000",
23
+ "timestamp": "2015-05-13T12:34:56Z",
24
+ "reason": "USER_INITIATED"
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'oj'
4
+ require 'rspec'
5
+ require File.expand_path('../../lib/alexa_skills_ruby', __FILE__)
6
+
7
+ MultiJson.use :oj
8
+
9
+ Dir[File.expand_path('../support/**/*', __FILE__)].each { |f| require f }
10
+
11
+ RSpec.configure do |config|
12
+
13
+ config.include FixtureSupport
14
+
15
+ end
@@ -0,0 +1,11 @@
1
+ module FixtureSupport
2
+
3
+ def load_json(name)
4
+ File.read(File.expand_path(File.dirname(__FILE__) + '/../fixtures/' + name))
5
+ end
6
+
7
+ def load_fixture(name)
8
+ Oj.load(load_json(name))
9
+ end
10
+
11
+ end
@@ -0,0 +1,142 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe AlexaSkillsRuby::Handler do
4
+
5
+ class TestHandler < AlexaSkillsRuby::Handler
6
+
7
+ attr_reader :auths, :intents, :launch, :ends, :starts
8
+
9
+ def initialize(*args)
10
+ super
11
+ @auths = []
12
+ @intents = []
13
+ @launch = []
14
+ @ends = []
15
+ @starts = []
16
+ end
17
+
18
+ on_session_start do
19
+ @starts << request
20
+ end
21
+
22
+ on_authenticate do
23
+ @auths << request
24
+ end
25
+
26
+ on_launch do
27
+ @launch << request
28
+ end
29
+
30
+ on_intent do
31
+ @intents << request
32
+ end
33
+
34
+ on_intent('special') do
35
+ @intents << request
36
+ end
37
+
38
+ on_session_end do
39
+ @ends << request
40
+ end
41
+ end
42
+
43
+ let(:handler) { TestHandler.new }
44
+
45
+ describe 'with a launch request' do
46
+ let(:request_json) { load_json 'example_launch.json' }
47
+
48
+ it 'fires the handlers' do
49
+ handler.handle(request_json)
50
+
51
+ expect(handler.auths.count).to eq 1
52
+ expect(handler.intents.count).to eq 0
53
+ expect(handler.ends.count).to eq 0
54
+ expect(handler.starts.count).to eq 1
55
+ expect(handler.launch.count).to eq 1
56
+ end
57
+ end
58
+
59
+ describe 'with an intent request' do
60
+ let(:request_json) { load_json 'example_intent.json' }
61
+
62
+ it 'fires the handlers' do
63
+ handler.handle(request_json)
64
+
65
+ expect(handler.auths.count).to eq 1
66
+ expect(handler.intents.count).to eq 1
67
+ expect(handler.ends.count).to eq 0
68
+ expect(handler.starts.count).to eq 0
69
+ expect(handler.launch.count).to eq 0
70
+ end
71
+ end
72
+
73
+ describe 'with a special intent request' do
74
+ let(:request_json) do
75
+ json = load_fixture 'example_intent.json'
76
+ json['request']['intent']['name'] = 'special'
77
+ Oj.dump(json)
78
+ end
79
+
80
+ it 'fires the handlers' do
81
+ handler.handle(request_json)
82
+
83
+ expect(handler.auths.count).to eq 1
84
+ expect(handler.intents.count).to eq 2
85
+ expect(handler.ends.count).to eq 0
86
+ expect(handler.starts.count).to eq 0
87
+ expect(handler.launch.count).to eq 0
88
+ end
89
+ end
90
+
91
+ describe 'with a session ended request' do
92
+ let(:request_json) { load_json 'example_session_ended.json' }
93
+
94
+ it 'fires the handlers' do
95
+ handler.handle(request_json)
96
+
97
+ expect(handler.auths.count).to eq 1
98
+ expect(handler.intents.count).to eq 0
99
+ expect(handler.ends.count).to eq 1
100
+ expect(handler.starts.count).to eq 0
101
+ expect(handler.launch.count).to eq 0
102
+ end
103
+ end
104
+
105
+ describe 'with an application_id set' do
106
+
107
+ let(:handler) { TestHandler.new({application_id: 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'}) }
108
+
109
+ describe 'with a valid app id in request' do
110
+ let(:request_json) { load_json 'example_session_ended.json' }
111
+
112
+ it 'fires the handlers' do
113
+ handler.handle(request_json)
114
+
115
+ expect(handler.auths.count).to eq 1
116
+ expect(handler.intents.count).to eq 0
117
+ expect(handler.ends.count).to eq 1
118
+ expect(handler.starts.count).to eq 0
119
+ expect(handler.launch.count).to eq 0
120
+ end
121
+ end
122
+
123
+ describe 'with an invalid app id in request' do
124
+ let(:request_json) do
125
+ json = load_fixture 'example_intent.json'
126
+ json['session']['application']['applicationId'] = 'broke'
127
+ Oj.dump(json)
128
+ end
129
+
130
+ it 'fires the handlers' do
131
+ expect { handler.handle(request_json) }.to raise_error AlexaSkillsRuby::InvalidApplicationId
132
+
133
+ expect(handler.auths.count).to eq 1
134
+ expect(handler.intents.count).to eq 0
135
+ expect(handler.ends.count).to eq 0
136
+ expect(handler.starts.count).to eq 0
137
+ expect(handler.launch.count).to eq 0
138
+ end
139
+ end
140
+ end
141
+
142
+ end
@@ -0,0 +1,59 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe AlexaSkillsRuby::JsonObject do
4
+
5
+ class TestJsonObject < AlexaSkillsRuby::JsonObject
6
+ attributes :name, :id
7
+ json_object_attribute :attr1, AlexaSkillsRuby::JsonObjects::User
8
+ end
9
+
10
+ class ChildTestJsonObject < TestJsonObject
11
+ attribute :another_field
12
+ json_object_attribute :attr2, AlexaSkillsRuby::JsonObjects::User
13
+ end
14
+
15
+ class OtherTestJsonObject < AlexaSkillsRuby::JsonObject
16
+ json_object_attribute :attr3, AlexaSkillsRuby::JsonObjects::User
17
+ end
18
+
19
+ describe '.json_object_attribute' do
20
+
21
+ it 'should keep separate lists for each type' do
22
+ expect(TestJsonObject._json_object_properties.keys).to contain_exactly :attr1
23
+ expect(ChildTestJsonObject._json_object_properties.keys).to contain_exactly :attr1, :attr2
24
+ expect(OtherTestJsonObject._json_object_properties.keys).to contain_exactly :attr3
25
+
26
+ expect(TestJsonObject._properties).to contain_exactly :name, :id
27
+ expect(ChildTestJsonObject._properties).to contain_exactly :name, :id, :another_field
28
+ expect(OtherTestJsonObject._properties.length).to eq 0
29
+ end
30
+
31
+ end
32
+
33
+ describe 'serialization' do
34
+
35
+ it 'serializes entities' do
36
+ json_object = TestJsonObject.new
37
+ json_object.attr1 = AlexaSkillsRuby::JsonObjects::User.new
38
+ json_object.attr1.user_id = 5
39
+ json_object.attr1.access_token = 'token'
40
+ json_object.name = 'name'
41
+ json_object.id = 1
42
+
43
+ json = json_object.as_json
44
+ expect(json).to eq({ 'attr1' => { 'userId' => 5, 'accessToken' => 'token'}, 'name' => 'name', 'id' => 1 })
45
+ end
46
+
47
+ end
48
+
49
+ describe 'deserialization' do
50
+ it 'populates the objects' do
51
+ json = { attr1: { id: 5, access_token: 'token'}, name: 'name', id: 1 }
52
+ json_object = TestJsonObject.new(json)
53
+ expect(json_object.name).to eq 'name'
54
+ expect(json_object.attr1).to be_a AlexaSkillsRuby::JsonObjects::User
55
+ expect(json_object.attr1.access_token).to eq 'token'
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,56 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe AlexaSkillsRuby::JsonObjects::SkillsRequest do
4
+
5
+ let(:launch_request_json) do
6
+ load_fixture 'example_launch.json'
7
+ end
8
+
9
+ let(:intent_request_json) do
10
+ load_fixture 'example_intent.json'
11
+ end
12
+
13
+ let(:session_ended_request_json) do
14
+ load_fixture 'example_session_ended.json'
15
+ end
16
+
17
+ it 'constructs a launch request' do
18
+ r = AlexaSkillsRuby::JsonObjects::SkillsRequest.new(launch_request_json)
19
+ expect(r).not_to be_nil
20
+ expect(r.version).to eq '1.0'
21
+ expect(r.session).to be_a AlexaSkillsRuby::JsonObjects::Session
22
+ expect(r.session.new).to eq true
23
+ expect(r.session.session_id).to eq 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000'
24
+ expect(r.session.application).to be_a AlexaSkillsRuby::JsonObjects::Application
25
+ expect(r.session.application.application_id).to eq 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
26
+ expect(r.session.attributes).to eq({})
27
+ expect(r.session.user).to be_a AlexaSkillsRuby::JsonObjects::User
28
+ expect(r.session.user.user_id).to eq 'amzn1.account.AM3B00000000000000000000000'
29
+ expect(r.request).to be_a AlexaSkillsRuby::JsonObjects::LaunchRequest
30
+ expect(r.request.type).to eq 'LaunchRequest'
31
+ expect(r.request.request_id).to eq 'amzn1.echo-api.request.0000000-0000-0000-0000-00000000000'
32
+ expect(r.request.timestamp).to eq '2015-05-13T12:34:56Z'
33
+ end
34
+
35
+ it 'constructs an intent request' do
36
+ r = AlexaSkillsRuby::JsonObjects::SkillsRequest.new(intent_request_json)
37
+ expect(r.request).to be_a AlexaSkillsRuby::JsonObjects::IntentRequest
38
+ expect(r.request.type).to eq 'IntentRequest'
39
+ expect(r.request.request_id).to eq 'amzn1.echo-api.request.0000000-0000-0000-0000-00000000000'
40
+ expect(r.request.timestamp).to eq '2015-05-13T12:34:56Z'
41
+ expect(r.request.intent).to be_a AlexaSkillsRuby::JsonObjects::Intent
42
+ expect(r.request.intent.name).to eq 'GetZodiacHoroscopeIntent'
43
+ expect(r.request.intent.slots).to be_a Hash
44
+ expect(r.request.intent.slots['ZodiacSign']).to eq('virgo')
45
+ end
46
+
47
+ it 'constructs a session ended request' do
48
+ r = AlexaSkillsRuby::JsonObjects::SkillsRequest.new(session_ended_request_json)
49
+ expect(r.request).to be_a AlexaSkillsRuby::JsonObjects::SessionEndedRequest
50
+ expect(r.request.type).to eq 'SessionEndedRequest'
51
+ expect(r.request.request_id).to eq 'amzn1.echo-api.request.0000000-0000-0000-0000-00000000000'
52
+ expect(r.request.timestamp).to eq '2015-05-13T12:34:56Z'
53
+ expect(r.request.reason).to eq 'USER_INITIATED'
54
+ end
55
+
56
+ end
@@ -0,0 +1,48 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe AlexaSkillsRuby::JsonObjects::SkillsResponse do
4
+
5
+ let(:response_json) do
6
+ {
7
+ 'version' => "1.0",
8
+ 'sessionAttributes' => {
9
+ 'supportedHoriscopePeriods' => {
10
+ 'daily' => true,
11
+ 'weekly' => false,
12
+ 'monthly' => false
13
+ }
14
+ },
15
+ 'response' => {
16
+ 'outputSpeech' => {
17
+ 'type' => "PlainText",
18
+ 'text' => "Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless. Can I help you with anything else?"
19
+ },
20
+ 'card' => {
21
+ 'type' => "Simple",
22
+ 'title' => "Horoscope",
23
+ 'content' => "Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless."
24
+ },
25
+ 'reprompt' => {
26
+ 'outputSpeech' => {
27
+ 'type' => "PlainText",
28
+ 'text' => "Can I help you with anything else?"
29
+ }
30
+ },
31
+ 'shouldEndSession' => false
32
+ }
33
+ }
34
+ end
35
+
36
+ it 'generates example json' do
37
+ sr = AlexaSkillsRuby::JsonObjects::SkillsResponse.new
38
+ r = sr.response
39
+ sr.session_attributes = {'supportedHoriscopePeriods' => {'daily' => true, 'weekly' => false, 'monthly' => false}}
40
+ r.set_output_speech_text("Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless. Can I help you with anything else?")
41
+ r.set_simple_card('Horoscope', 'Today will provide you a new learning opportunity. Stick with it and the possibilities will be endless.')
42
+ r.set_reprompt_speech_text('Can I help you with anything else?')
43
+ r.should_end_session = false
44
+
45
+ expect(sr.as_json).to eq response_json
46
+ end
47
+
48
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alexa_skills_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Dan Elbert
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 3.4.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.4.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: oj
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 2.10.2
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 2.10.2
97
+ description:
98
+ email:
99
+ - dan.elbert@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - Gemfile
106
+ - LICENSE
107
+ - README.md
108
+ - Rakefile
109
+ - alexa_skills_ruby.gemspec
110
+ - lib/alexa_skills_ruby.rb
111
+ - lib/alexa_skills_ruby/errors.rb
112
+ - lib/alexa_skills_ruby/handler.rb
113
+ - lib/alexa_skills_ruby/json_object.rb
114
+ - lib/alexa_skills_ruby/json_objects/application.rb
115
+ - lib/alexa_skills_ruby/json_objects/base_request.rb
116
+ - lib/alexa_skills_ruby/json_objects/card.rb
117
+ - lib/alexa_skills_ruby/json_objects/image.rb
118
+ - lib/alexa_skills_ruby/json_objects/intent.rb
119
+ - lib/alexa_skills_ruby/json_objects/intent_request.rb
120
+ - lib/alexa_skills_ruby/json_objects/launch_request.rb
121
+ - lib/alexa_skills_ruby/json_objects/output_speech.rb
122
+ - lib/alexa_skills_ruby/json_objects/reprompt.rb
123
+ - lib/alexa_skills_ruby/json_objects/response.rb
124
+ - lib/alexa_skills_ruby/json_objects/session.rb
125
+ - lib/alexa_skills_ruby/json_objects/session_ended_request.rb
126
+ - lib/alexa_skills_ruby/json_objects/skills_request.rb
127
+ - lib/alexa_skills_ruby/json_objects/skills_response.rb
128
+ - lib/alexa_skills_ruby/json_objects/user.rb
129
+ - lib/alexa_skills_ruby/version.rb
130
+ - spec/fixtures/example_intent.json
131
+ - spec/fixtures/example_launch.json
132
+ - spec/fixtures/example_response.json
133
+ - spec/fixtures/example_session_ended.json
134
+ - spec/spec_helper.rb
135
+ - spec/support/fixture_support.rb
136
+ - spec/unit/handler_spec.rb
137
+ - spec/unit/json_object_spec.rb
138
+ - spec/unit/json_objects/skills_request_spec.rb
139
+ - spec/unit/json_objects/skills_response_spec.rb
140
+ homepage: https://github.com/DanElbert/alexa_skills_ruby
141
+ licenses: []
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubyforge_project:
159
+ rubygems_version: 2.4.6
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: Simple library to interface with the Alexa Skills Kit
163
+ test_files:
164
+ - spec/fixtures/example_intent.json
165
+ - spec/fixtures/example_launch.json
166
+ - spec/fixtures/example_response.json
167
+ - spec/fixtures/example_session_ended.json
168
+ - spec/spec_helper.rb
169
+ - spec/support/fixture_support.rb
170
+ - spec/unit/handler_spec.rb
171
+ - spec/unit/json_object_spec.rb
172
+ - spec/unit/json_objects/skills_request_spec.rb
173
+ - spec/unit/json_objects/skills_response_spec.rb