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,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