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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +192 -0
- data/LICENSE.md +19 -0
- data/README.md +189 -0
- data/Rakefile +10 -0
- data/WRITING_PLUGINS.md +162 -0
- data/lib/farscape/agent.rb +113 -0
- data/lib/farscape/base_agent.rb +15 -0
- data/lib/farscape/cache.rb +13 -0
- data/lib/farscape/client/base_client.rb +27 -0
- data/lib/farscape/client/http_client.rb +99 -0
- data/lib/farscape/clients.rb +9 -0
- data/lib/farscape/errors.rb +172 -0
- data/lib/farscape/helpers/partially_ordered_list.rb +75 -0
- data/lib/farscape/logger.rb +13 -0
- data/lib/farscape/plugins.rb +71 -0
- data/lib/farscape/representor.rb +110 -0
- data/lib/farscape/transition.rb +81 -0
- data/lib/farscape/version.rb +6 -0
- data/lib/farscape.rb +4 -0
- data/lib/plugins/plugins.rb +104 -0
- data/spec/lib/farscape/cache_spec.rb +22 -0
- data/spec/lib/farscape/integration/entry_point_spec.rb +29 -0
- data/spec/lib/farscape/integration/interface_spec.rb +222 -0
- data/spec/lib/farscape/integration/representor_spec.rb +36 -0
- data/spec/lib/farscape/integration/resource_crud_spec.rb +92 -0
- data/spec/lib/farscape/logger_spec.rb +23 -0
- data/spec/lib/farscape/plugins_spec.rb +344 -0
- data/spec/lib/farscape/transition_spec.rb +79 -0
- data/spec/lib/helpers/partially_ordered_list_spec.rb +125 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/helpers.rb +5 -0
- data/tasks/yard.rake +15 -0
- metadata +221 -0
@@ -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
|
data/lib/farscape.rb
ADDED
@@ -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
|