farscape 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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