farscape 1.2.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.
@@ -0,0 +1,110 @@
1
+ require 'representors'
2
+ require 'farscape/transition'
3
+ require 'ostruct'
4
+
5
+ module Farscape
6
+ class SafeRepresentorAgent
7
+
8
+ include BaseAgent
9
+
10
+ attr_reader :agent
11
+ attr_reader :representor
12
+ attr_reader :response
13
+
14
+ EMPTY_BODIES = { hale: "{}" } #TODO: Fix Representor to allow nil resources
15
+
16
+ def initialize(requested_media_type, response, agent)
17
+ @agent = agent
18
+ @response = response
19
+ @requested_media_type = requested_media_type
20
+ @representor = deserialize(requested_media_type, response.body)
21
+ handle_extensions
22
+ end
23
+
24
+ %w(using omitting).each do |meth|
25
+ define_method(meth) { |name_or_type| self.class.new(@requested_media_type, @response, @agent.send(meth, name_or_type)) }
26
+ end
27
+
28
+ def attributes
29
+ representor.properties
30
+ end
31
+
32
+ #TODO: Handling list of transitions
33
+ def transitions
34
+ Hash[representor.transitions.map{ |trans| [trans.rel, Farscape::TransitionAgent.new(trans, agent)] }]
35
+ end
36
+
37
+ def embedded
38
+ Hash[representor.embedded.map{ |k, reps| [k, _embedded(reps, response)] }]
39
+ end
40
+
41
+ def to_hash
42
+ @representor.to_hash
43
+ end
44
+
45
+ def safe
46
+ reframe_representor(safe=true)
47
+ end
48
+
49
+ def unsafe
50
+ reframe_representor(safe=false)
51
+ end
52
+
53
+ private
54
+
55
+ def reframe_representor(safety)
56
+ agent = safety ? @agent.safe : @agent.unsafe
57
+ agent.representor.new(@requested_media_type, @response, agent)
58
+ end
59
+
60
+ def deserialize(requested_media_type, response_body)
61
+ return response_body unless requested_media_type
62
+ response_body = response_body || EMPTY_BODIES[@agent.media_type]
63
+ Representors::DeserializerFactory.build(requested_media_type, response_body).to_representor
64
+ end
65
+
66
+ def _embedded(reprs, response)
67
+ reprs = [reprs] unless reprs.respond_to?(:map)
68
+ reprs.map { |repr| @agent.representor.new(false, OpenStruct.new(status: response.status, headers: response.headers, body: repr), @agent) }
69
+ end
70
+
71
+ end
72
+
73
+ class RepresentorAgent < SafeRepresentorAgent
74
+ def method_missing(method, *args, &block)
75
+ super
76
+ rescue NoMethodError => e
77
+ parameters = args.first || {}
78
+ get_embedded(method) || get_transition(method, parameters, &block) || get_attribute(method) || raise
79
+ end
80
+
81
+ def respond_to_missing?(method_name, include_private = false)
82
+ super || [embedded.include?(method_name), method_transitions.include?(method), attributes.include?(method)].any?
83
+ end
84
+
85
+ # HACK! - Requires for method_missing; apparently an undocumented feature of Ruby
86
+ def to_ary
87
+ end
88
+
89
+ private
90
+
91
+ def get_embedded(meth)
92
+ embedded[meth.to_s]
93
+ end
94
+
95
+ def get_attribute(meth)
96
+ attributes[meth.to_s]
97
+ end
98
+
99
+ def get_transition(meth, request_params = {}, &block)
100
+ return false unless method_transitions.include?(meth = meth.to_s)
101
+ block = ->(*args) { args } unless block_given?
102
+ method_transitions[meth].invoke(request_params) { |x| block.call(x) }
103
+ end
104
+
105
+ def method_transitions
106
+ transitions.map { |k,v| @agent.client.safe_method?( v.interface_method ) ? {k => v} : {k+'!' => v} }.reduce({}, :merge)
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,81 @@
1
+ require 'representors'
2
+ require 'ostruct'
3
+ require 'active_support/core_ext/object/blank'
4
+
5
+ module Farscape
6
+
7
+ class TransitionAgent
8
+
9
+ include BaseAgent
10
+
11
+ def initialize(transition, agent)
12
+ @agent = agent
13
+ @transition = transition
14
+ handle_extensions
15
+ end
16
+
17
+ def invoke(args = {})
18
+ opts = OpenStruct.new
19
+ yield opts if block_given?
20
+ options = match_params(args, opts)
21
+ params = args.merge(options.parameters || {})
22
+
23
+ call_options = {}
24
+ call_options[:method] = @transition.interface_method
25
+ call_options[:headers] = @agent.get_accept_header(@agent.media_type).merge(options.headers || {})
26
+ call_options[:body] = options.attributes if options.attributes.present?
27
+
28
+ if call_options[:method].downcase == 'get'
29
+ # delegate the URL building to representors so we can use templated URIs
30
+ call_options[:url] = @transition.uri(params)
31
+ # still need to use this for extra params... (e.g. "conditions=can_do_anything")
32
+ if params.present?
33
+ if @transition.templated?
34
+ # exclude the parameters that have been consumed by Addressable (e.g. path segments) so
35
+ # we don't repeat those in the final URL (ex: /api{/uuid} => /api/123456, not /api/123456?uuid=123456)
36
+ # TODO: make some "variables" method in representors/transition.rb so we don't deal with this here
37
+ Addressable::Template.new(@transition.templated_uri).variables.each do |param|
38
+ params.delete(param.to_sym)
39
+ end
40
+ end
41
+ call_options[:params] = params
42
+ end
43
+ else
44
+ # Farscape handles "parameters" as query string, and "attributes" as request body.
45
+ # However, in many API documents, only "parameters" is used regardless of methods.
46
+ # Since changing API documents must have a huge impact on existing systems,
47
+ # we use parameters as the request body if the method is not GET.
48
+ # This makes it impossible to use URIs with parameters.
49
+ call_options[:url] = @transition.uri
50
+ call_options[:body] = (call_options[:body] || {}).merge(params) if params.present?
51
+ end
52
+
53
+ response = @agent.client.invoke(call_options)
54
+
55
+ @agent.find_exception(response)
56
+ end
57
+
58
+ %w(using omitting).each do |meth|
59
+ define_method(meth) { |name_or_type| self.class.new(@transition, @agent.send(meth, name_or_type)) }
60
+ end
61
+
62
+ def method_missing(meth, *args, &block)
63
+ @transition.send(meth, *args, &block)
64
+ end
65
+
66
+ private
67
+
68
+ def match_params(args, options)
69
+ [:parameters, :attributes].each do |key_type|
70
+ field_list = @transition.public_send(key_type)
71
+ field_names = field_list.map { |field| field.name.to_sym }
72
+ filtered_values = args.select { |k,_| field_names.include?(k) }
73
+ values = filtered_values.merge(options.public_send(key_type) || {})
74
+ options.public_send(:"#{key_type}=", values)
75
+ end
76
+ options
77
+ end
78
+
79
+
80
+ end
81
+ end
@@ -0,0 +1,6 @@
1
+ # Used to prevent the class/module from being loaded more than once
2
+ unless defined?(::Farscape::VERSION)
3
+ module Farscape
4
+ VERSION = '1.2.0'.freeze
5
+ end
6
+ end
data/lib/farscape.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'plugins/plugins'
2
+ require 'farscape/base_agent'
3
+ require 'farscape/agent'
4
+ require 'farscape/cache'
@@ -0,0 +1,104 @@
1
+ module Farscape
2
+ module Plugins
3
+
4
+ def self.enabled_plugins(plugins)
5
+ plugins.select { |plugin| plugins[plugin][:enabled] }
6
+ end
7
+
8
+ def self.disabled_plugins(plugins)
9
+ plugins.reject { |plugin| plugins[plugin][:enabled] }
10
+ end
11
+
12
+ # If the middleware has been disabled by name, return the name
13
+ # Else if by type, return the type.
14
+ # Else if :default_state was passed in return :default_state
15
+ def self.why_disabled(plugins, disabling_rules, options)
16
+ maybe = disabling_rules.map { |hash| hash.select { |k,v| k if v == options[k] } }
17
+ maybe |= [disabled_plugins(plugins)[options[:name]]]
18
+ maybe |= [:default_state] if options[:default_state] == :disabled
19
+ maybe.compact
20
+ end
21
+
22
+ def self.disabled?(plugins, disabling_rules, options)
23
+ options = normalize_selector(options)
24
+ return plugins[options[:name]][:enabled] if options.include?([:name])
25
+ why_disabled(plugins, disabling_rules, options).any?
26
+ end
27
+
28
+ def self.enabled?(plugins, disabling_rules, options)
29
+ !self.disabled?(plugins, disabling_rules, options)
30
+ end
31
+
32
+ def self.disable(name_or_type, disabling_rules, plugins)
33
+ name_or_type = self.normalize_selector(name_or_type)
34
+ plugins = set_plugin_states(name_or_type, false, plugins)
35
+ [disabling_rules << name_or_type, plugins]
36
+ end
37
+
38
+ def self.enable(name_or_type, disabling_rules, plugins)
39
+ name_or_type = normalize_selector(name_or_type)
40
+ plugins = set_plugin_states(name_or_type, true, plugins)
41
+ [disabling_rules.reject {|k| k == name_or_type}, plugins]
42
+ end
43
+
44
+ def self.set_plugin_states(name_or_type, condition, plugins)
45
+ plugins = Marshal.load( Marshal.dump(plugins) ) # TODO: This is super inefficient, figure out a good deep_dup
46
+ selected_plugins = find_attr_intersect(plugins, name_or_type)
47
+ selected_plugins.each { |plugin| plugins[plugin][:enabled] = condition }
48
+ plugins
49
+ end
50
+
51
+ def self.construct_stack(plugins)
52
+ stack = PartiallyOrderedList.new { |m,n| order_middleware(m,n) }
53
+ plugins.each do |_, plugin|
54
+ [*plugin[:middleware]].each do |middleware|
55
+ middleware = {class: middleware} unless middleware.is_a?(Hash)
56
+ middleware[:type] = plugin[:type]
57
+ middleware[:plugin] = plugin[:name]
58
+ stack.add(middleware)
59
+ end
60
+ end
61
+ stack
62
+ end
63
+
64
+ def self.normalize_selector(name_or_type)
65
+ name_or_type.is_a?(Hash) ? name_or_type : { name: name_or_type, type: name_or_type}
66
+ end
67
+
68
+ # Used by PartiallyOrderedList to implement the before: and after: options
69
+ def self.order_middleware(mw_1, mw_2)
70
+ case
71
+ when includes_middleware?(mw_1[:before],mw_2)
72
+ -1
73
+ when includes_middleware?(mw_1[:after],mw_2)
74
+ 1
75
+ when includes_middleware?(mw_2[:before],mw_1)
76
+ 1
77
+ when includes_middleware?(mw_2[:after],mw_1)
78
+ -1
79
+ end
80
+ end
81
+
82
+ def self.find_attr_intersect(master_hash, selector_hash)
83
+ master_hash.map do |mkey, mval|
84
+ selector_hash.map { |skey, sval| mkey if mval[skey] == sval }
85
+ end.flatten.compact
86
+ end
87
+
88
+ # Search a list for a given middleware by either its class or the type of its originating plugin
89
+ def self.includes_middleware?(list, middleware)
90
+ list = [*list]
91
+ list.map(&:to_s).include?(middleware[:class].to_s) || list.include?(middleware[:type])
92
+ end
93
+
94
+ def self.extensions(plugins)
95
+ plugs = plugins.map { |_, hash| hash[:extensions] }.compact
96
+ collect_values(plugs)
97
+ end
98
+
99
+ def self.collect_values(hashes)
100
+ hashes.reduce({}) { |h1, h2| h1.merge(h2) { |k, l1, l2| l1+l2 } }
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+ require 'farscape/cache'
3
+
4
+ describe(Farscape) do
5
+
6
+ describe 'cache' do
7
+
8
+ after(:each) { Farscape.cache = nil }
9
+
10
+ it 'defaults to a memory store' do
11
+ expect(Farscape.cache).to be_a(ActiveSupport::Cache::MemoryStore)
12
+ end
13
+
14
+ it 'can be set to any store' do
15
+ custom_cache = Object.new
16
+ Farscape.cache = custom_cache
17
+ expect(Farscape.cache).to eq(custom_cache)
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Farscape::Agent do
4
+ let(:entry_point) { "http://localhost:#{RAILS_PORT}"}
5
+
6
+ describe '#enter' do
7
+ it 'returns a Farscape::Representor' do
8
+ expect(Farscape::Agent.new(entry_point).enter).to be_a Farscape::RepresentorAgent
9
+ end
10
+
11
+ it 'can be provided an entry point after initialization' do
12
+ expect(Farscape::Agent.new.enter(entry_point)).to be_a Farscape::RepresentorAgent
13
+ end
14
+
15
+ #TODO decide on the appropriate error
16
+ it 'raises an appropriate error if no entry point is specified' do
17
+ expect{ Farscape::Agent.new.enter }.to raise_error(RuntimeError)
18
+ end
19
+
20
+ it 'raises an appropriate error when giving invalid requests' do
21
+ expect{ Farscape::Agent.new.enter("http://localhost:#{RAILS_PORT}/drds/ninja_boot")}.to raise_error(Farscape::Exceptions::UnprocessableEntity)
22
+ expect{ Farscape::Agent.new.enter("http://localhost:#{RAILS_PORT}/ninja_boot")}.to raise_error(Farscape::Exceptions::ProtocolException)
23
+
24
+ # Original test was expect{ Farscape::Agent.new.enter("http://localhost:#{RAILS_PORT}/ninja_boot")}.to raise_error(Farscape::Exceptions::NotFound)
25
+ # However, Moya is getting an internal server error when hitting a routing error instead of a 404
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,222 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ describe Farscape::SafeRepresentorAgent do
5
+
6
+ # TODO: Make Representor::Field behave like a string
7
+
8
+ let(:entry_point) { "http://localhost:#{RAILS_PORT}"}
9
+
10
+ let(:drds_link) { Farscape::Agent.new(entry_point).enter.transitions["drds"] }
11
+ let(:can_do_hash) { {conditions: 'can_do_anything'} }
12
+
13
+ describe "A Hypermedia API" do
14
+ it 'returns a Farscape::Representor instance
15
+ with a simple state-machine interface of attributes (data)
16
+ and transitions (link/form affordances) for interacting with the resource representations.' do
17
+ agent = Farscape::Agent.new(entry_point)
18
+ resources = agent.enter
19
+
20
+ expect(resources.attributes).to eq({}) # TODO: Make Crichton more flexible with Entry Points
21
+ expect(resources.transitions.keys).to eq(["drds"]) # => TODO: Add leviathans to Moya
22
+ end
23
+ end
24
+
25
+ describe "A Hypermedia Discovery Service" do
26
+ # TODO: Fix Documentation to reflect this
27
+
28
+ it 'follow your nose entry to select a registered resource' do
29
+ agent = Farscape::Agent.new(entry_point)
30
+ resources = agent.enter
31
+
32
+ expect(resources.transitions['drds'].invoke).to be_a Farscape::RepresentorAgent
33
+ end
34
+
35
+ it 'immediately loading a discoverable resource if known to be registered in the service a priori.' do
36
+ agent = Farscape::Agent.new
37
+ resources = agent.enter(entry_point)
38
+
39
+ expect(resources.transitions['drds'].invoke).to be_a Farscape::RepresentorAgent
40
+ end
41
+
42
+ it 'throws an error on an unknown resource' do
43
+ agent = Farscape::Agent.new
44
+
45
+ expect{ agent.enter }.to raise_error(RuntimeError) # TODO: Create Exact Error Interface for Farscape
46
+ end
47
+
48
+ end
49
+
50
+ context "API Interaction" do
51
+
52
+ let(:agent) { Farscape::Agent.new(entry_point) }
53
+ let(:drds) { agent.enter.transitions['drds'].invoke { |req| req.parameters = can_do_hash } }
54
+
55
+ describe "Load a Resource" do
56
+ it 'can load a resource' do
57
+ resources = agent.enter
58
+ drds_transition = resources.transitions['drds']
59
+ drds_resource = drds_transition.invoke { |req| req.parameters = can_do_hash }
60
+
61
+ expect(drds_resource.transitions['self'].uri).to eq(drds.transitions['self'].uri)
62
+ end
63
+ end
64
+
65
+ describe "A Representor Agent" do
66
+ it "Allows retrieving Response Header Information" do
67
+ resources = agent.enter
68
+ drds = resources.drds
69
+ expect(drds.response.status).to eq(200)
70
+ end
71
+ end
72
+
73
+ describe "Reload A Resource" do
74
+ it "can reload a resource" do
75
+ self_transition = drds.transitions['self']
76
+ reloaded_drds = self_transition.invoke
77
+
78
+ expect(reloaded_drds.to_hash).to eq(drds.to_hash)
79
+ end
80
+ end
81
+
82
+ describe "When using classmethods to refence API elements" do
83
+ context "When following unsafe transitions" do
84
+ it "can follow safe transitions" do
85
+ agent = Farscape::Agent.new
86
+ resources = agent.enter(entry_point)
87
+ expect(resources.drds).to be_a Farscape::RepresentorAgent
88
+ end
89
+ it "requires an ! on unsafe transitions" do
90
+ agent = Farscape::Agent.new
91
+ resources = agent.enter(entry_point)
92
+ expect { resources.drds { |req| req.parameters = can_do_hash }.create }.to raise_error(NoMethodError) # TODO: Create Exact Error Interface for Farscape
93
+
94
+ drones = resources.drds { |req| req.parameters = can_do_hash }
95
+ begin
96
+ drones.create!
97
+ raise StandardError
98
+ rescue Farscape::Exceptions::UnprocessableEntity => e
99
+ expect(e.representor.transitions.keys).to include('help')
100
+ end
101
+ end
102
+
103
+ it "can be called with arguments" do
104
+ agent = Farscape::Agent.new
105
+ resources = agent.enter(entry_point)
106
+ expect(resources.drds.search(status: 'active').items.all? { |h| h.status }).to be true
107
+ end
108
+
109
+ it "can take a block" do
110
+ agent = Farscape::Agent.new
111
+ resources = agent.enter(entry_point)
112
+ resources.drds { |req| req.parameters = can_do_hash }
113
+ resources.drds.search { |req| req.parameters = can_do_hash }
114
+
115
+ expect(resources.drds.search(status: 'activated') { |req| req.parameters = can_do_hash }.transitions.keys).to include('create')
116
+ expect(resources.drds.search(status: 'activated') { |req| req.parameters = can_do_hash }.items.all? { |h| h.status == 'activated' }).to be true
117
+ end
118
+ end
119
+ context "When Referencing Attributes" do
120
+ it "can reference attributes" do
121
+ resource = agent.enter(entry_point).drds(can_do_hash).items.first
122
+ expect(resource.status).to eq(drds.embedded['items'].first.attributes['status'])
123
+ end
124
+ it "is read only" do
125
+ resource = agent.enter(entry_point).drds(can_do_hash).items.first
126
+ expect {resource.status = 'ninja food'}.to raise_error(NoMethodError)
127
+ end
128
+ it "throws on unknown" do
129
+ resource = agent.enter(entry_point).drds(can_do_hash).items.find { |drd| drd.status == 'deactivated' }
130
+ expect {resource.ninja}.to raise_error(NoMethodError)
131
+ expect {resource.self {|r| r.parameters = {'conditions' => 'can_do_anything'}}.activate!.activate!}.to raise_error(NoMethodError, /undefined method `activate!' for/)
132
+ end
133
+ end
134
+ context "When handling namespace collisions" do
135
+ it "can be converted to a safe representor and back" do
136
+ resource = agent.safe.enter.transitions['drds'].invoke { |req| req.parameters = can_do_hash }.embedded['items'].first
137
+ expect {resource.status}.to raise_error(NoMethodError)
138
+
139
+ resource = resource.unsafe
140
+ expect(resource.status).to eq(drds.embedded['items'].first.attributes['status'])
141
+
142
+ resource = resource.safe
143
+ expect {resource.status}.to raise_error(NoMethodError)
144
+ end
145
+ end
146
+
147
+ context "When using Alternate Interface" do
148
+ it "can change to and from a safe representor" do
149
+ drds = agent.enter(entry_point).drds(can_do_hash)
150
+ safely = drds.safe
151
+ unsafely = safely.unsafe
152
+ expect(safely.attributes.keys).to eq(unsafely.attributes.keys) # TODO: Representor should support descriptor equality
153
+ expect(safely.transitions.keys).to eq(unsafely.transitions.keys) # TODO: Representor should support descriptor equality
154
+ expect(safely.embedded.keys).to eq(unsafely.embedded.keys) # TODO: Representor should support descriptor equality
155
+ end
156
+ end
157
+
158
+ end
159
+
160
+ context "Explore" do
161
+ describe "Apply Query Parameters" do
162
+ it 'allows Application of Query Parameters' do
163
+ search_transition = drds.transitions['search']
164
+
165
+ # TODO: Diverges from Doc due to doc not considering Field objects
166
+ expect(search_transition.parameters.map { |p| p.name } ).to eq(['status'])
167
+
168
+ filtered_drds = search_transition.invoke do |builder|
169
+ builder.parameters = { status: 'activated' }
170
+ end
171
+
172
+ expect(filtered_drds.transitions['items']).to_not be(nil)
173
+ end
174
+ end
175
+
176
+ describe "Transform Resource State" do
177
+ it 'allows Transformation of Resource State' do
178
+ embedded_drd_items = drds.embedded # TODO: Orig "drds.items" Looks like New Interface
179
+ drd = embedded_drd_items['items'].first
180
+
181
+ expect(drd.attributes.keys).to eq(["id", "name", "status", "old_status", "kind", "size", "leviathan_uuid", "created_at", "location", "location_detail", "destroyed_status"])
182
+ expect(drd.transitions.keys).to include("self", "update", "delete", "leviathan", "profile", "type", "help")
183
+
184
+ status = drd.attributes['status']
185
+ action = status == 'activated' ? 'deactivate' : 'activate'
186
+ deactivate_transition = drd.transitions[action]
187
+
188
+ # TODO: Not sure what to do about empty response bodies
189
+ # deactivated_drd = deactivate_transition.invoke
190
+ deactivate_transition.invoke { |req| req.parameters = can_do_hash }
191
+ deactivated_drd = drd.transitions['self'].invoke { |req| req.parameters = can_do_hash }
192
+
193
+ expect(deactivated_drd.attributes['status']).to_not eq(status)
194
+ expect(deactivated_drd.attributes.keys).to eq(drd.attributes.keys)
195
+ expect(deactivated_drd.transitions.keys).to_not eq(drd.transitions.keys)
196
+ end
197
+ end
198
+
199
+ describe "Transform Application State" do
200
+ xit 'allows Transformation of Application State' do
201
+ # TODO: Make Moya serve Leviathan as a separate service
202
+ # leviathan_transition = deactivated_drd.transitions['leviathan']
203
+ #
204
+ # leviathan = leviathan_transition.invoke
205
+ # leviathan.attributes # => { name: 'Elack' }
206
+ # leviathan.transitions # => ['self', 'drds']
207
+ #
208
+ # # Use Attributes
209
+ # create_transition = drds.transitions['create']
210
+ # create_transition.attributes # => ['name']
211
+ #
212
+ # new_drd = create_transition.invoke do |builder|
213
+ # builder.attributes = { name: 'Pike' }
214
+ # end
215
+ #
216
+ # new_drd.attributes # => { name: 'Pike' }
217
+ # new_drd.transitions # => ['self', 'edit', 'delete', 'deactivate', 'leviathan']
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Farscape::RepresentorAgent do
4
+ let(:entry_point) { "http://localhost:#{RAILS_PORT}"}
5
+
6
+ describe '#transitions' do
7
+ it 'returns a hash of tranistions' do
8
+ representor = Farscape::Agent.new(entry_point).enter
9
+ expect(representor.transitions["drds"].uri).to eq("http://localhost:1234/drds")
10
+ end
11
+
12
+ it 'responds to keys appropriately' do
13
+ representor = Farscape::Agent.new(entry_point).enter
14
+ expect(representor.transitions["drds"].invoke.transitions.keys).to eq(["self", "search", "items", "profile", "type", "help"])
15
+ end
16
+ end
17
+
18
+ describe "#invoke" do
19
+ it 'returns a representor' do
20
+ representor = Farscape::Agent.new(entry_point).enter
21
+ expect(representor.transitions["drds"].invoke).to be_a Farscape::RepresentorAgent
22
+ end
23
+
24
+ it 'can reload a resource' do
25
+ representor = Farscape::Agent.new(entry_point).enter.transitions["drds"].invoke
26
+ expect(representor.transitions["self"].invoke.to_hash).to eq(representor.to_hash)
27
+ end
28
+ end
29
+
30
+ describe '#attributes' do
31
+ it 'has readable attributes' do
32
+ representor = Farscape::Agent.new(entry_point).enter
33
+ expect(representor.transitions["drds"].invoke.attributes["total_count"]).to be > 4
34
+ end
35
+ end
36
+ end