farscape 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ require 'farscape/representor'
2
+ require 'farscape/clients'
3
+
4
+ module Farscape
5
+ class Agent
6
+
7
+ include BaseAgent
8
+
9
+ PROTOCOL = :http
10
+
11
+ attr_reader :media_type
12
+ attr_reader :entry_point
13
+
14
+ def initialize(entry = nil, media = :hale, safe = false, plugin_hash = {})
15
+ @entry_point = entry
16
+ @media_type = media
17
+ @safe_mode = safe
18
+ @plugin_hash = plugin_hash.empty? ? default_plugin_hash : plugin_hash
19
+ handle_extensions
20
+ end
21
+
22
+ def representor
23
+ safe? ? SafeRepresentorAgent : RepresentorAgent
24
+ end
25
+
26
+ def enter(entry = entry_point)
27
+ @entry_point ||= entry
28
+ raise "No Entry Point Provided!" unless entry
29
+ response = client.invoke(url: entry, headers: get_accept_header(media_type))
30
+ find_exception(response)
31
+ end
32
+
33
+ def find_exception(response)
34
+ error = client.dispatch_error(response)
35
+ begin
36
+ representing = representor.new(media_type, response, self)
37
+ rescue JSON::ParserError
38
+ representing = response
39
+ end
40
+ raise error.new(representing) if error
41
+ representing
42
+ end
43
+
44
+ # TODO share this information with serialization factory base
45
+ def get_accept_header(media_type)
46
+ media_types = { hale: 'application/vnd.hale+json' }
47
+ { 'Accept' => media_types[media_type] }
48
+ end
49
+
50
+ def client
51
+ Farscape.clients[PROTOCOL].new(self)
52
+ end
53
+
54
+ def safe
55
+ self.class.new(@entry_point, @media_type, true, @plugin_hash)
56
+ end
57
+
58
+ def unsafe
59
+ self.class.new(@entry_point, @media_type, false, @plugin_hash)
60
+ end
61
+
62
+ def safe?
63
+ @safe_mode
64
+ end
65
+
66
+ def plugins
67
+ @plugin_hash[:plugins]
68
+ end
69
+
70
+ def enabled_plugins
71
+ Plugins.enabled_plugins(@plugin_hash[:plugins])
72
+ end
73
+
74
+ def disabled_plugins
75
+ Plugins.disabled_plugins(@plugin_hash[:plugins])
76
+ end
77
+
78
+ def middleware_stack
79
+ @plugin_hash[:middleware_stack] ||= Plugins.construct_stack(enabled_plugins)
80
+ end
81
+
82
+ def using(name_or_type)
83
+ disabling_rules, plugins = Plugins.enable(name_or_type, @plugin_hash[:disabling_rules], @plugin_hash[:plugins])
84
+ plugin_hash = {
85
+ disabling_rules: disabling_rules,
86
+ plugins: plugins,
87
+ middleware_stack: nil
88
+ }
89
+ self.class.new(@entry_point, @media_type, @safe_mode, plugin_hash)
90
+ end
91
+
92
+ def omitting(name_or_type)
93
+ disabling_rules, plugins = Plugins.disable(name_or_type, @plugin_hash[:disabling_rules], @plugin_hash[:plugins])
94
+ plugin_hash = {
95
+ disabling_rules: disabling_rules,
96
+ plugins: plugins,
97
+ middleware_stack: nil
98
+ }
99
+ self.class.new(@entry_point, @media_type, @safe_mode, plugin_hash)
100
+ end
101
+
102
+ private
103
+
104
+ def default_plugin_hash
105
+ {
106
+ plugins: Farscape.plugins.dup, # A hash of plugins keyed by the plugin name
107
+ disabling_rules: Farscape.disabling_rules.dup, # A list of symbols that are Names or types of plugins
108
+ middleware_stack: nil
109
+ }
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,15 @@
1
+ module Farscape
2
+ module BaseAgent
3
+
4
+ def handle_extensions
5
+ extensions = Plugins.extensions(enabled_plugins)
6
+ extensions = extensions[self.class.to_s.split(':')[-1].to_sym]
7
+ extensions.each { |cls| self.extend(cls) } if extensions
8
+ end
9
+
10
+ %w(disabled_plugins enabled_plugins plugins).each do |meth|
11
+ define_method(meth) { @agent.send(meth) }
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/cache'
2
+
3
+ module Farscape
4
+
5
+ def self.cache
6
+ @cache ||= ActiveSupport::Cache::MemoryStore.new
7
+ end
8
+
9
+ def self.cache=(new_cache)
10
+ @cache = new_cache
11
+ end
12
+
13
+ end
@@ -0,0 +1,27 @@
1
+ module Farscape
2
+ class Agent
3
+ # Client independent of protocol, only used for HTTP for now
4
+ class BaseClient
5
+
6
+ def interface_methods
7
+ {
8
+ safe: [],
9
+ unsafe: [],
10
+ idempotent: []
11
+ }
12
+ end
13
+
14
+ def safe_method?(meth)
15
+ interface_methods[:safe].include?(meth)
16
+ end
17
+
18
+ def unsafe_method?(meth)
19
+ interface_methods[:unsafe].include?(meth)
20
+ end
21
+
22
+ def idempotent_method?(meth)
23
+ interface_methods[:idempotent].include?(meth)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ require 'faraday'
2
+ require 'farscape/client/base_client'
3
+ require 'farscape/plugins'
4
+ require 'farscape/errors'
5
+
6
+ module Farscape
7
+ class Agent
8
+ class HTTPClient < BaseClient
9
+
10
+ # The Faraday connection instance.
11
+ attr_reader :connection
12
+ attr_reader :Plugins
13
+
14
+ def initialize(agent)
15
+ @connection = Faraday.new do |builder|
16
+ builder.request :url_encoded
17
+ agent.middleware_stack.each do |middleware|
18
+ if middleware.key?(:config)
19
+ config = middleware[:config]
20
+ if config.is_a?(Array)
21
+ builder.use(middleware[:class], *config)
22
+ else
23
+ builder.use(middleware[:class], config)
24
+ end
25
+ else
26
+ builder.use(middleware[:class])
27
+ end
28
+ end
29
+
30
+ builder.adapter faraday_adapter
31
+ end
32
+ end
33
+
34
+ # Override this in a subclass to create clients with custom Faraday adapters
35
+ def faraday_adapter
36
+ Faraday.default_adapter
37
+ end
38
+
39
+ ##
40
+ # Makes a Faraday request given the specified options
41
+ #
42
+ # @options [Hash] The hash of Faraday options passed to the request, including url, method,
43
+ # params, body, and headers.
44
+ # @return [Faraday::Response] The response object resulting from the Faraday call
45
+ def invoke(options = {})
46
+ defaults = { method: 'get'}
47
+ options = defaults.merge(options)
48
+
49
+ connection.send(options[:method].to_s.downcase) do |req|
50
+ req.url options[:url]
51
+ req.body = options[:body] if options.has_key?(:body)
52
+ options[:params].each { |k,v| req.params[k] = v } if options.has_key?(:params)
53
+ options[:headers].each { |k,v| req.headers[k] = v } if options.has_key?(:headers)
54
+ end
55
+ end
56
+
57
+ def interface_methods
58
+ {
59
+ idempotent: ['PUT', 'DELETE'],
60
+ unsafe: ['POST', 'PATCH'], # http://tools.ietf.org/html/rfc5789
61
+ safe: ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']
62
+ }
63
+ end
64
+
65
+ def dispatch_error(response)
66
+ errors = Farscape::Exceptions
67
+ http_code = {
68
+ 400 => errors::BadRequest,
69
+ 401 => errors::Unauthorized,
70
+ 403 => errors::Forbidden,
71
+ 404 => errors::NotFound,
72
+ 405 => errors::MethodNotAllowed,
73
+ 406 => errors::NotAcceptable,
74
+ 407 => errors::ProxyAuthenticationRequired,
75
+ 408 => errors::RequestTimeout,
76
+ 409 => errors::Conflict,
77
+ 410 => errors::Gone,
78
+ 411 => errors::LengthRequired,
79
+ 412 => errors::PreconditionFailed,
80
+ 413 => errors::RequestEntityTooLarge,
81
+ 414 => errors::RequestUriTooLong,
82
+ 415 => errors::UnsupportedMediaType,
83
+ 416 => errors::RequestedRangeNotSatisfiable,
84
+ 417 => errors::ExpectationFailed,
85
+ 418 => errors::ImaTeapot,
86
+ 422 => errors::UnprocessableEntity,
87
+ 500 => errors::InternalServerError,
88
+ 501 => errors::NotImplemented,
89
+ 502 => errors::BadGateway,
90
+ 503 => errors::ServiceUnavailable,
91
+ 504 => errors::GatewayTimeout,
92
+ 505 => errors::ProtocolVersionNotSupported,
93
+ }
94
+ http_code[response.status] || errors::ProtocolException unless response.success?
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,9 @@
1
+ require 'farscape/client/http_client'
2
+
3
+ module Farscape
4
+
5
+ def self.clients
6
+ @clients ||= {http: Farscape::Agent::HTTPClient}
7
+ end
8
+
9
+ end
@@ -0,0 +1,172 @@
1
+ require 'active_support/core_ext/string/filters'
2
+
3
+ module Farscape
4
+ module Exceptions
5
+ class ProtocolException < IOError
6
+ attr_reader :representor
7
+
8
+ def initialize(representor)
9
+ @representor = representor
10
+ end
11
+
12
+ def message
13
+ @representor.representor.to_hash
14
+ end
15
+
16
+ def error_description
17
+ 'Unknown Error'
18
+ end
19
+ end
20
+
21
+ #4xx
22
+ class BadRequest < ProtocolException
23
+ def error_description
24
+ 'The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.'
25
+ end
26
+ end
27
+ class Unauthorized < ProtocolException
28
+ def error_description
29
+ 'The request requires user authentication.
30
+ The client MAY repeat the request with suitable Authorization.
31
+ If the request already included Authorization credentials,
32
+ then the response indicates that authorization has been refused for those credentials.'.squish
33
+ end
34
+ end
35
+ class Forbidden < ProtocolException
36
+ def error_description
37
+ 'The server understood the request, but is refusing to fulfill it.
38
+ Authorization will not help and the request SHOULD NOT be repeated.'.squish
39
+ end
40
+ end
41
+ class NotFound < ProtocolException
42
+ def error_description
43
+ 'The server has not found anything matching the Request-URI.
44
+ No indication is given of whether the condition is temporary or permanent.
45
+ This status code is commonly used when the server does not wish to reveal exactly why the request has been refused, or when no other response is applicable.'.squish
46
+ end
47
+ end
48
+ class MethodNotAllowed < ProtocolException
49
+ def error_description
50
+ 'The protocol method specified in the Request-Line is not allowed for the resource identified by the Request-URI.'
51
+ end
52
+ end
53
+ class NotAcceptable < ProtocolException
54
+ def error_description
55
+ 'The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request.'
56
+ end
57
+ end
58
+ class ProxyAuthenticationRequired < ProtocolException
59
+ def error_description
60
+ 'The client must first authenticate itself with the proxy.
61
+ The client MAY repeat the request with suitable Proxy Authorization.'.squish
62
+ end
63
+ end
64
+ class RequestTimeout < ProtocolException
65
+ def error_description
66
+ 'The client did not produce a request within the time that the server was prepared to wait.
67
+ The client MAY repeat the request without modifications at any later time.'.squish
68
+ end
69
+ end
70
+ class Conflict < ProtocolException
71
+ def error_description
72
+ 'The request could not be completed due to a conflict with the current state of the resource.
73
+ This code is only allowed in situations where it is expected that the user might be able to resolve the conflict and resubmit the request.
74
+ Conflicts are most likely to occur in response to an idempotent request.
75
+ For example, if versioning were being used and the entity included changes to a resource which conflict with those made by an earlier (third-party) request'.squish
76
+ end
77
+ end
78
+ class Gone < ProtocolException
79
+ def error_description
80
+ 'The requested resource is no longer available at the server and no forwarding address is known.
81
+ This condition is expected to be considered permanent.
82
+ Clients with link editing capabilities SHOULD delete references to the Request-URI after user approval.
83
+ This response is cacheable unless indicated otherwise.'.squish
84
+ end
85
+ end
86
+ class LengthRequired < ProtocolException
87
+ def error_description
88
+ 'The server refuses to accept the request without a defined content length.'
89
+ end
90
+ end
91
+ class PreconditionFailed < ProtocolException
92
+ def error_description
93
+ 'The precondition given by the client evaluated to false when it was tested on the server.'
94
+ end
95
+ end
96
+ class RequestEntityTooLarge < ProtocolException
97
+ def error_description
98
+ 'The server is refusing to process a request because the request entity is larger than the server is willing or able to process.'.squish
99
+ end
100
+ end
101
+ class RequestUriTooLong < ProtocolException
102
+ def error_description
103
+ 'The server is refusing to service the request because the Request-URI is longer than the server is willing to interpret.'
104
+ end
105
+ end
106
+ class UnsupportedMediaType < ProtocolException
107
+ def error_description
108
+ 'The server is refusing to service the request because the entity of the request is in a format not supported by the requested resource for the requested method.'
109
+ end
110
+ end
111
+ class RequestedRangeNotSatisfiable < ProtocolException
112
+ def error_description
113
+ 'A request requested a resource within a range,
114
+ and none of the range-specifier values in this field overlap the current extent of the selected resource,
115
+ and the request did not specify range conditions.'.squish
116
+ end
117
+ end
118
+ class ExpectationFailed < ProtocolException
119
+ def error_description
120
+ 'The expectation given by the client could not be met by this server.
121
+ If the server is a proxy, the server has unambiguous evidence that the request could not be met by the next-hop server.'.squish
122
+ end
123
+ end
124
+ class ImaTeapot < ProtocolException
125
+ def error_description
126
+ 'The server is a teapot; the resulting entity body may be short and stout.
127
+ Demonstrations of this behaviour exist.'.squish
128
+ end
129
+ end
130
+
131
+ class UnprocessableEntity < ProtocolException
132
+ def error_description
133
+ 'The request was well-formed but was unable to be followed due to semantic errors.'.squish
134
+ end
135
+ end
136
+
137
+ #5xx
138
+ class InternalServerError < ProtocolException
139
+ def error_description
140
+ 'The server encountered an unexpected condition which prevented it from fulfilling the request.'
141
+ end
142
+ end
143
+ class NotImplemented < ProtocolException
144
+ def error_description
145
+ 'The server does not support the functionality required to fulfill the request.'
146
+ end
147
+ end
148
+ class BadGateway < ProtocolException
149
+ def error_description
150
+ 'The server received an invalid response from the upstream server it accessed in attempting to fulfill the request.'
151
+ end
152
+ end
153
+ class ServiceUnavailable < ProtocolException
154
+ def error_description
155
+ 'The server is currently unable to handle the request due to a temporary overloading or maintenance of the server.
156
+ The implication is that this is a temporary condition which will be alleviated after some delay.'.squish
157
+ end
158
+ end
159
+ class GatewayTimeout < ProtocolException
160
+ def error_description
161
+ 'The server did not receive a timely response from an upstream server specified it needed to access in attempting to complete the request.'
162
+ end
163
+ end
164
+ class ProtocolVersionNotSupported < ProtocolException
165
+ def error_description
166
+ 'The server does not support, or refuses to support, the protocol or protocol version that was used in the request message.
167
+ The server is indicating that it is unable or unwilling to complete the request using the same protocol as the client'.squish
168
+ end
169
+ end
170
+
171
+ end
172
+ end
@@ -0,0 +1,75 @@
1
+ # Convenience class that finds an order for a set of elements that satisfies an arbitrary sorting function that
2
+ # can be undefined for some pairs.
3
+ # TODO: Optimize and gemify (or find existing gem that does this)
4
+
5
+ class PartiallyOrderedList
6
+ include Enumerable
7
+
8
+ class Element < Struct.new(:item, :preceders)
9
+ def inspect
10
+ if preceders.any?
11
+ "#{item} > {#{preceders.map(&:item).join(', ')}}"
12
+ else
13
+ item
14
+ end
15
+ end
16
+ def ==(other)
17
+ item == other.item
18
+ end
19
+ end
20
+
21
+ CircularOrderingError = Class.new(StandardError)
22
+
23
+ attr_accessor :elements, :ordering
24
+
25
+ def initialize(&block)
26
+ raise ArgumentError, "#{self.class}.new requires a block" unless block_given?
27
+ @elements = []
28
+ @ordering = block
29
+ end
30
+
31
+ def add(item)
32
+ @cached_ary = nil
33
+ new_el = Element.new(item, [])
34
+ elements.each do |old_el|
35
+ case ordering.call(old_el.item, new_el.item)
36
+ when -1
37
+ new_el.preceders << old_el
38
+ when 1
39
+ old_el.preceders << new_el
40
+ end
41
+ end
42
+ elements << new_el
43
+ end
44
+
45
+ def delete(item)
46
+ if element = elements.find { |elt| elt.item == item }
47
+ elements.delete(element)
48
+ @cached_ary.delete(element) if @cached_ary
49
+ elements.each { |elt| elt.preceders.delete(element) }
50
+ item
51
+ end
52
+ end
53
+
54
+ def each(&block)
55
+ return to_enum unless block_given?
56
+ if @cached_ary && @cached_ary.size == elements.size
57
+ @cached_ary.each{ |elt| yield elt.item }
58
+ else
59
+ @cached_ary = []
60
+ unadded = elements.map{ |elt| elt=elt.dup; elt.preceders = elt.preceders.dup; elt }
61
+ while unadded.any?
62
+ i = unadded.find_index { |candidate| candidate.preceders.none? }
63
+ if i
64
+ to_add = unadded.delete_at(i)
65
+ yield(to_add.item)
66
+ unadded.each { |elt| elt.preceders.delete(to_add) }
67
+ @cached_ary << to_add
68
+ else
69
+ raise CircularOrderingError.new("Could not resolve ordering for #{unadded.map(&:item)}")
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ end
@@ -0,0 +1,13 @@
1
+ require 'logger'
2
+
3
+ module Farscape
4
+
5
+ def self.logger
6
+ @logger ||= Logger.new(STDOUT)
7
+ end
8
+
9
+ def self.logger=(new_logger)
10
+ @logger = new_logger
11
+ end
12
+
13
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'helpers/partially_ordered_list'
2
+
3
+ module Farscape
4
+
5
+ extend Plugins
6
+
7
+ attr_reader :plugins
8
+ attr_reader :disabling_rules
9
+
10
+ @plugins = {} # A hash of plugins keyed by the plugin name
11
+ @disabling_rules = [] # A list of symbols that are Names or types of plugins
12
+ @middleware_stack = nil
13
+
14
+ def self.plugins
15
+ @plugins
16
+ end
17
+
18
+ def self.disabling_rules
19
+ @disabling_rules
20
+ end
21
+
22
+ def self.register_plugin(options)
23
+ @middleware_stack = nil
24
+ options[:enabled] = self.enabled?(options)
25
+ @plugins[options[:name]] = options
26
+ end
27
+
28
+ def self.register_plugins(a_list)
29
+ a_list.each { |options| register_plugin(options) }
30
+ end
31
+
32
+ # Returns the Poset representing middleware dependency
33
+ def self.middleware_stack
34
+ @middleware_stack ||= Plugins.construct_stack(enabled_plugins)
35
+ end
36
+
37
+ def self.enabled_plugins
38
+ Plugins.enabled_plugins(@plugins)
39
+ end
40
+
41
+ def self.disabled_plugins
42
+ Plugins.disabled_plugins(@plugins)
43
+ end
44
+
45
+ def self.disabled?(options)
46
+ Plugins.disabled?(@plugins, @disabling_rules, options)
47
+ end
48
+
49
+ def self.enabled?(options)
50
+ Plugins.enabled?(@plugins, @disabling_rules, options)
51
+ end
52
+
53
+ # Prevents a plugin from being registered, and disables it if it's already there
54
+ def self.disable!(name_or_type)
55
+ @middleware_stack = nil
56
+ @disabling_rules, @plugins = Plugins.disable(name_or_type, @disabling_rules, @plugins)
57
+ end
58
+
59
+ # Allows a plugin to be registered, and enables it if it's already there
60
+ def self.enable!(name_or_type)
61
+ @middleware_stack = nil
62
+ @disabling_rules, @plugins = Plugins.enable(name_or_type, @disabling_rules, @plugins)
63
+ end
64
+
65
+ # Removes all plugins and disablings of plugins
66
+ def self.clear
67
+ @plugins = {}
68
+ @disabling_rules = []
69
+ @middleware_stack = nil
70
+ end
71
+ end