playful 0.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +6 -0
  7. data/History.rdoc +3 -0
  8. data/LICENSE.rdoc +22 -0
  9. data/README.rdoc +194 -0
  10. data/Rakefile +20 -0
  11. data/features/control_point.feature +13 -0
  12. data/features/device.feature +22 -0
  13. data/features/device_discovery.feature +9 -0
  14. data/features/step_definitions/control_point_steps.rb +19 -0
  15. data/features/step_definitions/device_discovery_steps.rb +40 -0
  16. data/features/step_definitions/device_steps.rb +28 -0
  17. data/features/support/common.rb +9 -0
  18. data/features/support/env.rb +17 -0
  19. data/features/support/fake_upnp_device_collection.rb +108 -0
  20. data/features/support/world_extensions.rb +15 -0
  21. data/lib/core_ext/hash_patch.rb +5 -0
  22. data/lib/core_ext/socket_patch.rb +16 -0
  23. data/lib/core_ext/to_upnp_s.rb +65 -0
  24. data/lib/playful.rb +5 -0
  25. data/lib/playful/control_point.rb +175 -0
  26. data/lib/playful/control_point/base.rb +74 -0
  27. data/lib/playful/control_point/device.rb +511 -0
  28. data/lib/playful/control_point/error.rb +13 -0
  29. data/lib/playful/control_point/service.rb +404 -0
  30. data/lib/playful/device.rb +28 -0
  31. data/lib/playful/logger.rb +8 -0
  32. data/lib/playful/ssdp.rb +195 -0
  33. data/lib/playful/ssdp/broadcast_searcher.rb +114 -0
  34. data/lib/playful/ssdp/error.rb +6 -0
  35. data/lib/playful/ssdp/listener.rb +38 -0
  36. data/lib/playful/ssdp/multicast_connection.rb +112 -0
  37. data/lib/playful/ssdp/network_constants.rb +17 -0
  38. data/lib/playful/ssdp/notifier.rb +41 -0
  39. data/lib/playful/ssdp/searcher.rb +87 -0
  40. data/lib/playful/version.rb +3 -0
  41. data/lib/rack/upnp_control_point.rb +70 -0
  42. data/playful.gemspec +38 -0
  43. data/spec/spec_helper.rb +16 -0
  44. data/spec/support/search_responses.rb +134 -0
  45. data/spec/unit/core_ext/to_upnp_s_spec.rb +105 -0
  46. data/spec/unit/playful/control_point/device_spec.rb +7 -0
  47. data/spec/unit/playful/control_point_spec.rb +45 -0
  48. data/spec/unit/playful/ssdp/listener_spec.rb +29 -0
  49. data/spec/unit/playful/ssdp/multicast_connection_spec.rb +157 -0
  50. data/spec/unit/playful/ssdp/notifier_spec.rb +76 -0
  51. data/spec/unit/playful/ssdp/searcher_spec.rb +110 -0
  52. data/spec/unit/playful/ssdp_spec.rb +214 -0
  53. data/tasks/control_point.html +30 -0
  54. data/tasks/control_point.thor +43 -0
  55. data/tasks/search.thor +128 -0
  56. data/tasks/test_js/FABridge.js +1425 -0
  57. data/tasks/test_js/WebSocketMain.swf +807 -0
  58. data/tasks/test_js/swfobject.js +825 -0
  59. data/tasks/test_js/web_socket.js +1133 -0
  60. data/test/test_ssdp.rb +298 -0
  61. data/test/test_ssdp_notification.rb +74 -0
  62. data/test/test_ssdp_response.rb +31 -0
  63. data/test/test_ssdp_search.rb +23 -0
  64. metadata +339 -0
@@ -0,0 +1,17 @@
1
+ module Playful
2
+ class SSDP
3
+ module NetworkConstants
4
+
5
+ BROADCAST_IP = '255.255.255.255'
6
+
7
+ # Default multicast IP address
8
+ MULTICAST_IP = '239.255.255.250'
9
+
10
+ # Default multicast port
11
+ MULTICAST_PORT = 1900
12
+
13
+ # Default TTL
14
+ TTL = 4
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ require_relative '../logger'
2
+ require_relative 'multicast_connection'
3
+
4
+
5
+ class Playful::SSDP::Notifier < Playful::SSDP::MulticastConnection
6
+ include LogSwitch::Mixin
7
+
8
+ def initialize(nt, usn, ddf_url, valid_for_duration)
9
+ @os = RbConfig::CONFIG['host_vendor'].capitalize + '/' +
10
+ RbConfig::CONFIG['host_os']
11
+ @upnp_version = '1.0'
12
+ @notification = notification(nt, usn, ddf_url, valid_for_duration)
13
+ end
14
+
15
+ def post_init
16
+ if send_datagram(@notification, MULTICAST_IP, MULTICAST_PORT) > 0
17
+ log "Sent notification:\n#{@notification}"
18
+ end
19
+ end
20
+
21
+ # @param [String] nt "Notification Type"; a potential search target. Used in
22
+ # +NT+ header.
23
+ # @param [String] usn "Unique Service Name"; a composite identifier for the
24
+ # advertisement. Used in +USN+ header.
25
+ # @param [String] ddf_url Device Description File URL for the root device.
26
+ # @param [Fixnum] valid_for_duration Duration in seconds for which the
27
+ # advertisement is valid. Used in +CACHE-CONTROL+ header.
28
+ def notification(nt, usn, ddf_url, valid_for_duration)
29
+ <<-NOTIFICATION
30
+ NOTIFY * HTTP/1.1\r
31
+ HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
32
+ CACHE-CONTROL: max-age=#{valid_for_duration}\r
33
+ LOCATION: #{ddf_url}\r
34
+ NT: #{nt}\r
35
+ NTS: ssdp:alive\r
36
+ SERVER: #{@os} UPnP/#{@upnp_version} Playful/#{Playful::VERSION}\r
37
+ USN: #{usn}\r
38
+ \r
39
+ NOTIFICATION
40
+ end
41
+ end
@@ -0,0 +1,87 @@
1
+ require_relative '../logger'
2
+ require_relative 'multicast_connection'
3
+
4
+ module Playful
5
+
6
+ # A subclass of an EventMachine::Connection, this handles doing M-SEARCHes.
7
+ #
8
+ # Search types:
9
+ # ssdp:all
10
+ # upnp:rootdevice
11
+ # uuid:[device-uuid]
12
+ # urn:schemas-upnp-org:device:[deviceType-version]
13
+ # urn:schemas-upnp-org:service:[serviceType-version]
14
+ # urn:[custom-schema]:device:[deviceType-version]
15
+ # urn:[custom-schema]:service:[serviceType-version]
16
+ class SSDP::Searcher < SSDP::MulticastConnection
17
+ include LogSwitch::Mixin
18
+
19
+ DEFAULT_RESPONSE_WAIT_TIME = 5
20
+ DEFAULT_M_SEARCH_COUNT = 2
21
+
22
+ # @return [EventMachine::Channel] Provides subscribers with responses from
23
+ # their search request.
24
+ attr_reader :discovery_responses
25
+
26
+ # @param [String] search_target
27
+ # @param [Hash] options
28
+ # @option options [Fixnum] response_wait_time
29
+ # @option options [Fixnum] ttl
30
+ # @option options [Fixnum] m_search_count The number of times to send the
31
+ # M-SEARCH. UPnP 1.0 suggests to send the request more than once.
32
+ def initialize(search_target, options = {})
33
+ options[:ttl] ||= TTL
34
+ options[:response_wait_time] ||= DEFAULT_RESPONSE_WAIT_TIME
35
+ @m_search_count = options[:m_search_count] ||= DEFAULT_M_SEARCH_COUNT
36
+
37
+ @search = m_search(search_target, options[:response_wait_time])
38
+
39
+ super options[:ttl]
40
+ end
41
+
42
+ # This is the callback called by EventMachine when it receives data on the
43
+ # socket that's been opened for this connection. In this case, the method
44
+ # parses the SSDP responses/notifications into Hashes and adds them to the
45
+ # appropriate EventMachine::Channel (provided as accessor methods). This
46
+ # effectively means that in each Channel, you get a Hash that represents
47
+ # the headers for each response/notification that comes in on the socket.
48
+ #
49
+ # @param [String] response The data received on this connection's socket.
50
+ def receive_data(response)
51
+ ip, port = peer_info
52
+ log "Response from #{ip}:#{port}:\n#{response}\n"
53
+ parsed_response = parse(response)
54
+
55
+ return if parsed_response.has_key? :nts
56
+ return if parsed_response[:man] &&
57
+ parsed_response[:man] =~ /ssdp:discover/
58
+
59
+ @discovery_responses << parsed_response
60
+ end
61
+
62
+ # Sends the M-SEARCH that was built during init. Logs what was sent if the
63
+ # send was successful.
64
+ def post_init
65
+ @m_search_count.times do
66
+ if send_datagram(@search, MULTICAST_IP, MULTICAST_PORT) > 0
67
+ log "Sent datagram search:\n#{@search}"
68
+ end
69
+ end
70
+ end
71
+
72
+ # Builds the M-SEARCH request string.
73
+ #
74
+ # @param [String] search_target
75
+ # @param [Fixnum] response_wait_time
76
+ def m_search(search_target, response_wait_time)
77
+ <<-MSEARCH
78
+ M-SEARCH * HTTP/1.1\r
79
+ HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
80
+ MAN: "ssdp:discover"\r
81
+ MX: #{response_wait_time}\r
82
+ ST: #{search_target}\r
83
+ \r
84
+ MSEARCH
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module Playful
2
+ VERSION = '0.1.0.alpha.1'
3
+ end
@@ -0,0 +1,70 @@
1
+ require 'rack'
2
+ require_relative '../playful/control_point'
3
+
4
+ module Rack
5
+
6
+ # Middleware that allows your Rack app to keep tabs on devices that the
7
+ # {Playful::ControlPoint} has found. UPnP devices that match the +search_type+
8
+ # are discovered and added to the list, then removed as those devices send out
9
+ # +ssdp:byebye+ notifications. All of this depends on +EventMachine::Channel+s,
10
+ # and thus requires that an EventMachine reactor is running. If you don't
11
+ # have one running, the {Playful::ControlPoint} will start one for you.
12
+ #
13
+ # @example Control all root devices
14
+ #
15
+ # Thin::Server.start('0.0.0.0', 3000) do
16
+ # use Rack::UPnPControlPoint, search_type: :root
17
+ #
18
+ # map "/devices" do
19
+ # run lambda { |env|
20
+ # devices = env['upnp.devices']
21
+ # friendly_names = devices.map(&:friendly_name).join("\n")
22
+ # [200, {'Content-Type' => 'text/plain'}, [friendly_names]]
23
+ # }
24
+ # end
25
+ # end
26
+ #
27
+ class UPnPControlPoint
28
+
29
+ # @param [Rack::Builder] app Your Rack application.
30
+ # @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
31
+ # @see Playful::SSDP::Searcher
32
+ def initialize(app, options = {})
33
+ @app = app
34
+ @devices = []
35
+ options[:search_type] ||= :root
36
+ EM.next_tick { start_control_point(options[:search_type], options) }
37
+ end
38
+
39
+ # Creates and starts the {Playful::ControlPoint}, then manages the list of
40
+ # devices using the +EventMachine::Channel+ objects yielded in.
41
+ #
42
+ # @param [Symbol,String] search_type The device(s) you want to search for
43
+ # and control.
44
+ # @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
45
+ # @see Playful::SSDP::Searcher
46
+ def start_control_point(search_type, options)
47
+ @cp = ::Playful::ControlPoint.new(search_type, options)
48
+
49
+ @cp.start do |new_device_channel, old_device_channel|
50
+ new_device_channel.subscribe do |notification|
51
+ @devices << notification
52
+ end
53
+
54
+ old_device_channel.subscribe do |old_device|
55
+ @devices.reject! { |d| d.usn == old_device[:usn] }
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ # Adds the whole list of devices to <tt>env['upnp.devices']</tt> so that
62
+ # that list can be accessed from within your app.
63
+ #
64
+ # @param [Hash] env The Rack environment.
65
+ def call(env)
66
+ env['upnp.devices'] = @devices
67
+ @app.call(env)
68
+ end
69
+ end
70
+ end
data/playful.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'playful/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'playful'
7
+ s.version = Playful::VERSION
8
+ s.author = 'turboladen'
9
+ s.email = 'steve.loveless@gmail.com'
10
+ s.homepage = 'http://github.com/turboladen/playful'
11
+ s.summary = 'Use me to build a UPnP app!'
12
+ s.description = %q{playful provides the tools you need to build an app that runs
13
+ in a UPnP environment.}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.extra_rdoc_files = %w(History.rdoc README.rdoc)
19
+ s.require_paths = ['lib']
20
+ s.required_ruby_version = Gem::Requirement.new('>=1.9.1')
21
+
22
+ s.add_dependency 'eventmachine', '>=1.0.0'
23
+ s.add_dependency 'em-http-request', '>=1.0.2'
24
+ s.add_dependency 'em-synchrony'
25
+ s.add_dependency 'nori', '>=2.0.2'
26
+ s.add_dependency 'log_switch', '>=0.4.0'
27
+ s.add_dependency 'savon', '~>2.0'
28
+
29
+ s.add_development_dependency 'bundler'
30
+ s.add_development_dependency 'cucumber', '>=1.0.0'
31
+ s.add_development_dependency 'em-websocket', '>=0.3.6'
32
+ s.add_development_dependency 'rake'
33
+ s.add_development_dependency 'rspec', '>=3.0.0.beta'
34
+ s.add_development_dependency 'simplecov', '>=0.4.2'
35
+ s.add_development_dependency 'thin'
36
+ s.add_development_dependency 'thor', '>=0.1.6'
37
+ s.add_development_dependency 'yard', '>=0.7.0'
38
+ end
@@ -0,0 +1,16 @@
1
+ require 'simplecov'
2
+
3
+ SimpleCov.start
4
+
5
+ require 'coveralls'
6
+ Coveralls.wear!
7
+
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |file| require file }
11
+
12
+ ENV['RUBY_UPNP_ENV'] = 'testing'
13
+
14
+ RSpec.configure do |config|
15
+ #config.raise_errors_for_deprecations!
16
+ end
@@ -0,0 +1,134 @@
1
+ SSDP_SEARCH_RESPONSES_PARSED = {
2
+ root_device1: {
3
+ cache_control: 'max-age=1200',
4
+ date: 'Mon, 26 Sep 2011 06:40:19 GMT',
5
+ location: 'http://192.168.10.3:5001/description/fetch',
6
+ server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
7
+ st: 'upnp:rootdevice',
8
+ ext: nil,
9
+ usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice',
10
+ content_length: 0
11
+ },
12
+ root_device2: {
13
+ cache_control: 'max-age=1200',
14
+ date: 'Mon, 26 Sep 2011 06:40:20 GMT',
15
+ location: 'http://192.168.10.4:5001/description/fetch',
16
+ server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
17
+ st: 'upnp:rootdevice',
18
+ ext: nil,
19
+ usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice',
20
+ content_length: 0
21
+ },
22
+ media_server: {
23
+ cache_control: 'max-age=1200',
24
+ date: 'Mon, 26 Sep 2011 06:40:21 GMT',
25
+ location: 'http://192.168.10.3:5001/description/fetch',
26
+ server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
27
+ st: 'urn:schemas-upnp-org:device:MediaServer:1',
28
+ ext: nil,
29
+ usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:1',
30
+ content_length: 0
31
+ }
32
+ }
33
+
34
+ SSDP_DESCRIPTIONS = {
35
+ root_device1: {
36
+ 'root' => {
37
+ 'specVersion' => {
38
+ 'major' => '1',
39
+ 'minor' => '0'
40
+ },
41
+ 'URLBase' => 'http://192.168.10.3:5001/',
42
+ 'device' => {
43
+ 'dlna:X_DLNADOC' => %w[DMS-1.50 M-DMS-1.50],
44
+ 'deviceType' => 'urn:schemas-upnp-org:device:MediaServer:1',
45
+ 'friendlyName' => 'PS3 Media Server [gutenberg]',
46
+ 'manufacturer' => 'PMS',
47
+ 'manufacturerURL' => 'http://ps3mediaserver.blogspot.com',
48
+ 'modelDescription' => 'UPnP/AV 1.0 Compliant Media Server',
49
+ 'modelName' => 'PMS',
50
+ 'modelNumber' => '01',
51
+ 'modelURL' => 'http://ps3mediaserver.blogspot.com',
52
+ 'serialNumber' => nil,
53
+ 'UPC' => nil,
54
+ 'UDN' => 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d',
55
+ 'iconList' => {
56
+ 'icon' => {
57
+ 'mimetype' => 'image/jpeg',
58
+ 'width' => '120',
59
+ 'height' => '120',
60
+ 'depth' => '24',
61
+ 'url' => '/images/icon-256.png'
62
+ }
63
+ },
64
+ 'presentationURL' => 'http://192.168.10.3:5001/console/index.html',
65
+ 'serviceList' => {
66
+ 'service' => [
67
+ {
68
+ 'serviceType' => 'urn:schemas-upnp-org:service:ContentDirectory:1',
69
+ 'serviceId' => 'urn:upnp-org:serviceId:ContentDirectory',
70
+ 'SCPDURL' => '/UPnP_AV_ContentDirectory_1.0.xml',
71
+ 'controlURL' => '/upnp/control/content_directory',
72
+ 'eventSubURL' => '/upnp/event/content_directory'
73
+ },
74
+ {
75
+ 'serviceType' => 'urn:schemas-upnp-org:service:ConnectionManager:1',
76
+ 'serviceId' => 'urn:upnp-org:serviceId:ConnectionManager',
77
+ 'SCPDURL' => '/UPnP_AV_ConnectionManager_1.0.xml',
78
+ 'controlURL' => '/upnp/control/connection_manager',
79
+ 'eventSubURL' => '/upnp/event/connection_manager'
80
+ }
81
+ ]
82
+ }
83
+ },
84
+ '@xmlns:dlna' => 'urn:schemas-dlna-org:device-1-0',
85
+ '@xmlns' => 'urn:schemas-upnp-org:device-1-0'
86
+ }
87
+ }
88
+ }
89
+
90
+ ROOT_DEVICE1 = <<-RD
91
+ HTTP/1.1 200 OK
92
+ CACHE-CONTROL: max-age=1200
93
+ DATE: Mon, 26 Sep 2011 06:40:19 GMT
94
+ LOCATION: http://1.2.3.4:5678/description/fetch
95
+ SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
96
+ ST: upnp:rootdevice
97
+ EXT:
98
+ USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice
99
+ Content-Length: 0
100
+
101
+ RD
102
+
103
+ ROOT_DEVICE2 = <<-RD
104
+ HTTP/1.1 200 OK
105
+ CACHE-CONTROL: max-age=1200
106
+ DATE: Mon, 26 Sep 2011 06:40:20 GMT
107
+ LOCATION: http://1.2.3.4:5678/description/fetch
108
+ SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
109
+ ST: upnp:rootdevice
110
+ EXT:
111
+ USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice
112
+ Content-Length: 0
113
+
114
+ RD
115
+
116
+ MEDIA_SERVER = <<-MD
117
+ HTTP/1.1 200 OK
118
+ CACHE-CONTROL: max-age=1200
119
+ DATE: Mon, 26 Sep 2011 06:40:21 GMT
120
+ LOCATION: http://1.2.3.4:5678/description/fetch
121
+ SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
122
+ ST: urn:schemas-upnp-org:device:MediaServer:1
123
+ EXT:
124
+ USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:
125
+ Content-Length: 0
126
+
127
+ MD
128
+
129
+ RESPONSES = {
130
+ root_device1: ROOT_DEVICE1,
131
+ root_device2: ROOT_DEVICE2,
132
+ media_server: MEDIA_SERVER
133
+ }
134
+
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+ require 'core_ext/to_upnp_s'
3
+
4
+
5
+ describe Hash do
6
+ describe '#to_upnp_s' do
7
+ context ':uuid as key' do
8
+ it "returns a String like 'uuid:[Hash value]'" do
9
+ expect({ uuid: '12345' }.to_upnp_s).to eq 'uuid:12345'
10
+ end
11
+
12
+ it "doesn't check if the Hash value is legit" do
13
+ expect({ uuid: '' }.to_upnp_s).to eq 'uuid:'
14
+ end
15
+ end
16
+
17
+ context ':device_type as key' do
18
+ context 'domain name not given' do
19
+ it "returns a String like 'urn:schemas-upnp-org:device:[device type]'" do
20
+ expect({ device_type: '12345' }.to_upnp_s).
21
+ to eq 'urn:schemas-upnp-org:device:12345'
22
+ end
23
+
24
+ it "doesn't check if the Hash value is legit" do
25
+ expect({ device_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:device:'
26
+ end
27
+ end
28
+
29
+ context 'domain name given' do
30
+ it "returns a String like 'urn:[domain name]:device:[device type]'" do
31
+ expect({ device_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
32
+ to eq 'urn:my-domain:device:12345'
33
+ end
34
+
35
+ it "doesn't check if the Hash value is legit" do
36
+ expect({ device_type: '', domain_name: 'stuff' }.to_upnp_s).
37
+ to eq 'urn:stuff:device:'
38
+ end
39
+ end
40
+ end
41
+
42
+ context ':service_type as key' do
43
+ context 'domain name not given' do
44
+ it "returns a String like 'urn:schemas-upnp-org:service:[service type]'" do
45
+ expect({ service_type: '12345' }.to_upnp_s).
46
+ to eq 'urn:schemas-upnp-org:service:12345'
47
+ end
48
+
49
+ it "doesn't check if the Hash value is legit" do
50
+ expect({ service_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:service:'
51
+ end
52
+ end
53
+
54
+ context 'domain name given' do
55
+ it "returns a String like 'urn:[domain name]:service:[service type]'" do
56
+ expect({ service_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
57
+ to eq 'urn:my-domain:service:12345'
58
+ end
59
+
60
+ it "doesn't check if the Hash value is legit" do
61
+ expect({ service_type: '', domain_name: 'my-domain' }.to_upnp_s).
62
+ to eq 'urn:my-domain:service:'
63
+ end
64
+ end
65
+ end
66
+
67
+ context 'some other Hash key' do
68
+ context 'domain name not given' do
69
+ it 'returns self.to_s' do
70
+ expect({ firestorm: 12345 }.to_upnp_s).to eq '{:firestorm=>12345}'
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ describe Symbol do
78
+ context ':all' do
79
+ describe '#to_upnp_s' do
80
+ it "returns 'ssdp:all'" do
81
+ expect(:all.to_upnp_s).to eq 'ssdp:all'
82
+ end
83
+ end
84
+ end
85
+
86
+ context ':root' do
87
+ describe '#to_upnp_s' do
88
+ it "returns 'upnp:rootdevice'" do
89
+ expect(:root.to_upnp_s).to eq 'upnp:rootdevice'
90
+ end
91
+ end
92
+ end
93
+
94
+ it "returns itself if one of the defined shortcuts wasn't given" do
95
+ expect(:firestorm.to_upnp_s).to eq :firestorm
96
+ end
97
+ end
98
+
99
+ describe String do
100
+ it 'returns itself' do
101
+ expect('Stuff and things'.to_upnp_s).to eq 'Stuff and things'
102
+ end
103
+ end
104
+
105
+