frenetic 0.0.20.alpha.6 → 1.0.0.alpha.1
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/.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
|