frenetic 0.0.20.alpha.6 → 1.0.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -1
- data/Appraisals +9 -0
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/frenetic.gemspec +8 -5
- data/gemfiles/faraday_08.gemfile +10 -0
- data/gemfiles/faraday_08.gemfile.lock +77 -0
- data/gemfiles/faraday_09.gemfile +10 -0
- data/gemfiles/faraday_09.gemfile.lock +77 -0
- data/lib/frenetic.rb +57 -30
- data/lib/frenetic/briefly_memoizable.rb +34 -0
- data/lib/frenetic/concerns/collection_rest_methods.rb +1 -1
- data/lib/frenetic/concerns/hal_linked.rb +5 -35
- data/lib/frenetic/concerns/member_rest_methods.rb +0 -2
- data/lib/frenetic/concerns/structured.rb +0 -5
- data/lib/frenetic/connection.rb +110 -0
- data/lib/frenetic/errors.rb +112 -0
- data/lib/frenetic/hypermedia_link.rb +74 -0
- data/lib/frenetic/hypermedia_link_set.rb +43 -0
- data/lib/frenetic/middleware/hal_json.rb +9 -12
- data/lib/frenetic/resource.rb +22 -6
- data/lib/frenetic/resource_collection.rb +0 -1
- data/lib/frenetic/resource_mockery.rb +55 -1
- data/lib/frenetic/version.rb +1 -1
- data/spec/{concerns/breifly_memoizable_spec.rb → briefly_memoizable_spec.rb} +10 -18
- data/spec/concerns/hal_linked_spec.rb +49 -62
- data/spec/concerns/member_rest_methods_spec.rb +8 -10
- data/spec/concerns/structured_spec.rb +70 -75
- data/spec/connection_spec.rb +137 -0
- data/spec/fixtures/test_api_requests.rb +8 -2
- data/spec/frenetic_spec.rb +221 -133
- data/spec/hypermedia_link_set_spec.rb +155 -0
- data/spec/hypermedia_link_spec.rb +153 -0
- data/spec/middleware/hal_json_spec.rb +13 -15
- data/spec/resource_collection_spec.rb +17 -16
- data/spec/resource_mockery_spec.rb +69 -0
- data/spec/resource_spec.rb +110 -63
- data/spec/support/rspec.rb +0 -1
- metadata +88 -75
- data/lib/frenetic/concerns/briefly_memoizable.rb +0 -34
- data/lib/frenetic/concerns/configurable.rb +0 -59
- data/lib/frenetic/concerns/resource_mockery.rb +0 -48
- data/lib/frenetic/configuration.rb +0 -88
- data/spec/concerns/configurable_spec.rb +0 -50
- data/spec/concerns/resource_mockery_spec.rb +0 -56
- data/spec/configuration_spec.rb +0 -134
@@ -1,6 +1,5 @@
|
|
1
1
|
class Frenetic
|
2
2
|
module Structured
|
3
|
-
|
4
3
|
# Stores the unique signature of each Resource Structure
|
5
4
|
# Used to determine when a Structure has changed and thus
|
6
5
|
# needs to be redefined.
|
@@ -28,9 +27,7 @@ class Frenetic
|
|
28
27
|
|
29
28
|
def rebuild_structure!
|
30
29
|
destroy_structure!
|
31
|
-
|
32
30
|
@@signatures[struct_key] = signature
|
33
|
-
|
34
31
|
Struct.new( struct_key, *@attrs.keys )
|
35
32
|
end
|
36
33
|
|
@@ -44,9 +41,7 @@ class Frenetic
|
|
44
41
|
|
45
42
|
def destroy_structure!
|
46
43
|
return unless structure_defined?
|
47
|
-
|
48
44
|
@@signatures.delete struct_key
|
49
|
-
|
50
45
|
Struct.send :remove_const, struct_key
|
51
46
|
end
|
52
47
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
class Frenetic
|
4
|
+
class Connection < SimpleDelegator
|
5
|
+
attr_reader :builder_config, :connection_config, :errors
|
6
|
+
|
7
|
+
ConnectionConfigKeys = [:url, :params, :headers, :request, :ssl, :proxy]
|
8
|
+
|
9
|
+
def initialize(config = {})
|
10
|
+
@errors = {}
|
11
|
+
process_config(config)
|
12
|
+
validate_configuration!
|
13
|
+
|
14
|
+
connection = Faraday.new(connection_config) do |builder|
|
15
|
+
configure_authentication(builder)
|
16
|
+
configure_responder(builder)
|
17
|
+
configure_cache(builder)
|
18
|
+
configure_adapter(builder)
|
19
|
+
end
|
20
|
+
super(connection)
|
21
|
+
end
|
22
|
+
|
23
|
+
def valid?
|
24
|
+
@errors = {}
|
25
|
+
@errors[:adapter] = 'must be present' if !@config[:adapter]
|
26
|
+
@errors[:url] = 'must be present' if !@config[:url]
|
27
|
+
@errors.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_config(raw_cfg)
|
31
|
+
@config = {}.merge(raw_cfg.to_hash)
|
32
|
+
@config[:url] = Addressable::URI.parse(raw_cfg[:url])
|
33
|
+
cfgs = @config.inject({builder:{}, conn:{}}) do |conf, (k,v)|
|
34
|
+
if ConnectionConfigKeys.include?(k)
|
35
|
+
conf[:conn][k] = v
|
36
|
+
else
|
37
|
+
conf[:builder][k] = v
|
38
|
+
end
|
39
|
+
conf
|
40
|
+
end
|
41
|
+
[
|
42
|
+
@builder_config = cfgs[:builder],
|
43
|
+
@connection_config = cfgs[:conn]
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
def configure_authentication(builder)
|
48
|
+
use_basic_auth(builder) if builder_config[:username]
|
49
|
+
use_token_auth(builder) if builder_config[:api_token]
|
50
|
+
end
|
51
|
+
|
52
|
+
def configure_responder(builder)
|
53
|
+
builder.response(:hal_json)
|
54
|
+
end
|
55
|
+
|
56
|
+
def configure_cache(builder)
|
57
|
+
case builder_config[:cache]
|
58
|
+
when :rack then use_rack_cache(builder)
|
59
|
+
when :rails then use_rails_cache(builder)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def configure_adapter(builder)
|
64
|
+
builder.adapter(builder_config[:adapter])
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def validate_configuration!
|
70
|
+
raise ConfigError.new(self) if !valid?
|
71
|
+
end
|
72
|
+
|
73
|
+
def use_basic_auth(builder)
|
74
|
+
builder.request :basic_auth, builder_config[:username], builder_config[:password]
|
75
|
+
end
|
76
|
+
|
77
|
+
def use_token_auth(builder)
|
78
|
+
builder.request :token_auth, builder_config[:api_token]
|
79
|
+
end
|
80
|
+
|
81
|
+
def use_rack_cache(builder)
|
82
|
+
require_lib('rack-cache', 'Frenetic Rack::Cache caching strategy')
|
83
|
+
builder.use(
|
84
|
+
FaradayMiddleware::RackCompatible,
|
85
|
+
Rack::Cache::Context,
|
86
|
+
{
|
87
|
+
metastore: "file:tmp/rack/meta/#{cache_key}",
|
88
|
+
entitystore: "file:tmp/rack/body/#{cache_key}",
|
89
|
+
ignore_headers: %w{Authorization Set-Cookie X-Content-Digest}
|
90
|
+
}
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def use_rails_cache(builder)
|
95
|
+
require_lib 'faraday-http-cache', 'Frenetic Rails caching strategy'
|
96
|
+
builder.use(Faraday::HttpCache, store:Rails.cache, logger:Rails.logger)
|
97
|
+
end
|
98
|
+
|
99
|
+
def cache_key
|
100
|
+
Digest::MD5.hexdigest connection_config[:url].hostname
|
101
|
+
end
|
102
|
+
|
103
|
+
def require_lib(lib = nil, context = nil)
|
104
|
+
lib ? require(lib) : yield
|
105
|
+
rescue NameError, LoadError => err
|
106
|
+
context ||= self
|
107
|
+
raise ConfigError, "Could not load required `#{lib}` dependency for #{context}: #{err.message}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'active_support/core_ext/array/conversions'
|
3
|
+
|
4
|
+
class Frenetic
|
5
|
+
# Generic Frenetic exception class.
|
6
|
+
Error = Class.new(StandardError)
|
7
|
+
|
8
|
+
# Raised when there is a configuration error
|
9
|
+
class ConfigError < Error
|
10
|
+
def initialize(model)
|
11
|
+
@model = model
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
if @model.is_a? String
|
17
|
+
@model
|
18
|
+
else
|
19
|
+
errs = @model.errors.collect do |key, msg|
|
20
|
+
"#{key.to_s.titleize} #{msg}"
|
21
|
+
end
|
22
|
+
"Invalid Configuration: #{errs.to_sentence}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Raised when there is a Hypermedia error
|
28
|
+
HypermediaError = Class.new(Error)
|
29
|
+
|
30
|
+
# Raised when there is a Link Template error
|
31
|
+
LinkTemplateError = Class.new(Error)
|
32
|
+
|
33
|
+
# Raised when a Resource does not have a mock class defined.
|
34
|
+
#
|
35
|
+
# For example:
|
36
|
+
#
|
37
|
+
# class Widget < Frenetic::Resource
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# class MockWidget < Widget
|
41
|
+
# include Frenetic::ResourceMockery
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# Would correctly create the necessary Mock Resource
|
45
|
+
class UndefinedResourceMock < Error
|
46
|
+
attr_reader :namespace, :resource
|
47
|
+
def initialize(namespace, resource)
|
48
|
+
@namespace = namespace
|
49
|
+
@resource = resource
|
50
|
+
end
|
51
|
+
|
52
|
+
def message
|
53
|
+
"Mock resource not defined for `#{namespace}`." \
|
54
|
+
" Create a new class that inherits from `#{resource}` and mixin" \
|
55
|
+
" `Frenetic::ResourceMockery` to define a mock."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Parent class for all specific exceptions which are raised as a result of a
|
60
|
+
# network response.
|
61
|
+
class ResponseError < Error
|
62
|
+
attr_reader :env, :error, :method, :status, :url
|
63
|
+
def initialize(env)
|
64
|
+
env ||= {}
|
65
|
+
body = env.fetch(:body, {})
|
66
|
+
@env = env
|
67
|
+
@error = body['error']
|
68
|
+
@method = env[:method]
|
69
|
+
@status = env[:status]
|
70
|
+
@url = env[:url]
|
71
|
+
super(message)
|
72
|
+
end
|
73
|
+
|
74
|
+
def message
|
75
|
+
@error
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Raised when a network response returns a 400-level error
|
80
|
+
ClientError = Class.new(ResponseError)
|
81
|
+
|
82
|
+
# Raised when a network response returns a 500-level error
|
83
|
+
ServerError = Class.new(ResponseError)
|
84
|
+
|
85
|
+
# Parent class for all specific exceptions which are raised as a result of a
|
86
|
+
# parsing the network request response body.
|
87
|
+
class ParsingError < ResponseError
|
88
|
+
def message
|
89
|
+
"#{status} Error"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Raised when there is a problem parsing the response body of a 400-level error
|
94
|
+
ClientParsingError = Class.new(ParsingError)
|
95
|
+
|
96
|
+
# Raised when there is a problem parsing the response body of a 400-level error
|
97
|
+
ServerParsingError = Class.new(ParsingError)
|
98
|
+
|
99
|
+
# Raised when there is a problem parsing the response body of an otherwise
|
100
|
+
# successful network request. Provides access to the original exception also.
|
101
|
+
class UnknownParsingError < ParsingError
|
102
|
+
attr_reader :original_exception
|
103
|
+
def initialize(env, err)
|
104
|
+
@original_exception = err
|
105
|
+
super(env)
|
106
|
+
end
|
107
|
+
|
108
|
+
def message
|
109
|
+
@original_exception.message
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'addressable/template'
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
|
4
|
+
class Frenetic
|
5
|
+
class HypermediaLink
|
6
|
+
def initialize( link )
|
7
|
+
@link = link.with_indifferent_access
|
8
|
+
end
|
9
|
+
|
10
|
+
def href( tmpl_data = {} )
|
11
|
+
if templated?
|
12
|
+
expand tmpl_data
|
13
|
+
else
|
14
|
+
@link['href']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
alias :to_url :href
|
18
|
+
|
19
|
+
def templated?
|
20
|
+
return false unless hash?
|
21
|
+
|
22
|
+
@link['templated'] == true
|
23
|
+
end
|
24
|
+
|
25
|
+
def expandable?( tmpl_data )
|
26
|
+
return false unless templated?
|
27
|
+
|
28
|
+
tmpl_data = normalize_data(tmpl_data)
|
29
|
+
|
30
|
+
(template.variables & tmpl_data.keys.map(&:to_s)).size == template.variables.size
|
31
|
+
end
|
32
|
+
|
33
|
+
def template
|
34
|
+
@template ||= Addressable::Template.new @link['href']
|
35
|
+
end
|
36
|
+
|
37
|
+
def as_json
|
38
|
+
@link
|
39
|
+
end
|
40
|
+
|
41
|
+
def rel
|
42
|
+
@link['rel']
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def expand( tmpl_data )
|
48
|
+
tmpl_data = normalize_data(tmpl_data)
|
49
|
+
|
50
|
+
return template.expand( tmpl_data ).to_s if expandable? tmpl_data
|
51
|
+
|
52
|
+
raise Frenetic::HypermediaError,
|
53
|
+
"The data provided could not satisfy the template requirements.\n" \
|
54
|
+
" Template: #{template.pattern}\n" \
|
55
|
+
" Data: #{tmpl_data}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def hash?
|
59
|
+
@link.is_a? Hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def normalize_data( data )
|
63
|
+
return data if data.is_a? Hash
|
64
|
+
|
65
|
+
infer_template_values data
|
66
|
+
end
|
67
|
+
|
68
|
+
def infer_template_values( data )
|
69
|
+
key = template.variables.first
|
70
|
+
|
71
|
+
{ key => data }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
|
4
|
+
require 'frenetic/hypermedia_link'
|
5
|
+
|
6
|
+
class Frenetic
|
7
|
+
class HypermediaLinkSet < Delegator
|
8
|
+
def initialize( link_set = [] )
|
9
|
+
link_set = [link_set] unless link_set.is_a? Array
|
10
|
+
|
11
|
+
@link_set = link_set.map do |link|
|
12
|
+
if link.is_a? HypermediaLink
|
13
|
+
link
|
14
|
+
else
|
15
|
+
HypermediaLink.new link
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def href( tmpl_vars = {} )
|
21
|
+
return @link_set.first.href if tmpl_vars.blank?
|
22
|
+
|
23
|
+
link = find_relevant_link( tmpl_vars ) and link.href( tmpl_vars )
|
24
|
+
end
|
25
|
+
|
26
|
+
def []( relation )
|
27
|
+
@link_set.find{ |link| link.rel == relation.to_s }
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_relevant_link( tmpl_vars )
|
31
|
+
@link_set.find{ |link| link.expandable? tmpl_vars } or
|
32
|
+
raise Frenetic::HypermediaError,
|
33
|
+
"Could not find a relevant link for the data provided.\n" \
|
34
|
+
"Are any of the links missing the templated:true property?\n" \
|
35
|
+
" Template Data: #{tmpl_vars}\n" \
|
36
|
+
" Link Set: #{@link_set.collect(&:as_json)}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def __getobj__
|
40
|
+
@link_set
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -7,23 +7,20 @@ class Frenetic
|
|
7
7
|
def process_response(env)
|
8
8
|
super
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
raise ClientError, env[:body]['error']
|
10
|
+
case env[:status]
|
11
|
+
when 500...599 then raise ServerError.new(env)
|
12
|
+
when 400...499 then raise ClientError.new(env)
|
14
13
|
end
|
15
14
|
rescue Faraday::Error::ParsingError => err
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
else
|
21
|
-
raise ParsingError, err.message
|
15
|
+
case env[:status]
|
16
|
+
when 500...599 then raise ServerParsingError.new(env)
|
17
|
+
when 400...499 then raise ClientParsingError.new(env)
|
18
|
+
else raise UnknownParsingError.new(env, err)
|
22
19
|
end
|
23
20
|
end
|
24
|
-
|
25
21
|
end
|
26
22
|
end
|
27
23
|
end
|
28
24
|
|
29
|
-
Faraday.register_middleware
|
25
|
+
Faraday::Response.register_middleware \
|
26
|
+
hal_json:lambda { Frenetic::Middleware::HalJson }
|
data/lib/frenetic/resource.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'ostruct'
|
2
3
|
require 'active_support/inflector'
|
3
4
|
require 'active_support/core_ext/hash/indifferent_access'
|
4
5
|
|
@@ -37,14 +38,12 @@ class Frenetic
|
|
37
38
|
end
|
38
39
|
|
39
40
|
def self.properties
|
41
|
+
return mock_class.default_attributes if test_mode?
|
40
42
|
(api.schema[namespace]||{})['properties'] or raise HypermediaError, %Q{Could not find schema definition for the resource "#{namespace}"}
|
41
43
|
end
|
42
44
|
|
43
45
|
def self.mock_class
|
44
|
-
@mock_class or raise Frenetic::
|
45
|
-
"Mock resource not defined for #{namespace}." \
|
46
|
-
" Subclass #{self} and mixin Frenetic::ResourceMockery" \
|
47
|
-
" to define a mock"
|
46
|
+
@mock_class or raise Frenetic::UndefinedResourceMock.new(namespace, self)
|
48
47
|
end
|
49
48
|
|
50
49
|
def self.as_mock( params = {} )
|
@@ -53,12 +52,14 @@ class Frenetic
|
|
53
52
|
|
54
53
|
def initialize( p = {} )
|
55
54
|
build_params p
|
56
|
-
@attrs
|
55
|
+
@attrs = {}
|
57
56
|
|
58
57
|
properties.keys.each do |k|
|
59
58
|
@attrs[k] = @params[k]
|
60
59
|
end
|
61
60
|
|
61
|
+
extract_embedded_resources
|
62
|
+
|
62
63
|
build_structure
|
63
64
|
end
|
64
65
|
|
@@ -91,7 +92,7 @@ class Frenetic
|
|
91
92
|
"#{k}=#{val}"
|
92
93
|
end.join(' ')
|
93
94
|
|
94
|
-
ivars = (instance_variables - [:@structure]).map do |k|
|
95
|
+
ivars = (instance_variables - [:@structure, :@attributes]).map do |k|
|
95
96
|
val = instance_variable_get k
|
96
97
|
val = val.is_a?(String) ? "\"#{val}\"" : val || 'nil'
|
97
98
|
|
@@ -110,6 +111,21 @@ class Frenetic
|
|
110
111
|
@params = (p || {}).with_indifferent_access
|
111
112
|
end
|
112
113
|
|
114
|
+
def extract_embedded_resources
|
115
|
+
class_namespace = self.class.to_s.deconstantize
|
116
|
+
|
117
|
+
@params.fetch('_embedded',{}).each do |k,v|
|
118
|
+
class_name = "#{class_namespace}::#{k.classify}"
|
119
|
+
klass = class_name.constantize rescue OpenStruct
|
120
|
+
|
121
|
+
@attrs[k] = if self.class.test_mode? || is_a?(ResourceMockery)
|
122
|
+
klass.as_mock v
|
123
|
+
else
|
124
|
+
klass.new v
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
113
129
|
def build_structure
|
114
130
|
@structure = structure.new( *@attrs.values )
|
115
131
|
end
|