frenetic 1.0.0.alpha.1 → 1.0.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 +4 -4
- data/.editorconfig +16 -0
- data/.gitignore +1 -1
- data/.irbrc +1 -1
- data/.rubocop.yml +45 -0
- data/.travis.yml +2 -2
- data/Appraisals +1 -1
- data/LICENSE +1 -1
- data/README.md +15 -15
- data/Rakefile +1 -1
- data/gemfiles/faraday_08.gemfile.lock +8 -8
- data/gemfiles/faraday_09.gemfile.lock +8 -8
- data/lib/frenetic.rb +13 -10
- data/lib/frenetic/briefly_memoizable.rb +9 -7
- data/lib/frenetic/concerns/collection_rest_methods.rb +4 -5
- data/lib/frenetic/concerns/hal_linked.rb +12 -9
- data/lib/frenetic/concerns/member_rest_methods.rb +7 -10
- data/lib/frenetic/concerns/structured.rb +2 -2
- data/lib/frenetic/connection.rb +25 -17
- data/lib/frenetic/errors.rb +67 -2
- data/lib/frenetic/hypermedia_link.rb +19 -26
- data/lib/frenetic/hypermedia_link_set.rb +11 -14
- data/lib/frenetic/middleware/hal_json.rb +3 -4
- data/lib/frenetic/resource.rb +31 -25
- data/lib/frenetic/resource_collection.rb +3 -3
- data/lib/frenetic/resource_mockery.rb +7 -5
- data/lib/frenetic/version.rb +2 -2
- data/spec/briefly_memoizable_spec.rb +1 -1
- data/spec/concerns/hal_linked_spec.rb +5 -5
- data/spec/concerns/member_rest_methods_spec.rb +1 -1
- data/spec/concerns/structured_spec.rb +6 -5
- data/spec/connection_spec.rb +16 -4
- data/spec/fixtures/test_api_requests.rb +32 -28
- data/spec/frenetic_spec.rb +3 -3
- data/spec/hypermedia_link_set_spec.rb +3 -3
- data/spec/hypermedia_link_spec.rb +1 -1
- data/spec/middleware/hal_json_spec.rb +3 -3
- data/spec/resource_collection_spec.rb +3 -4
- data/spec/resource_mockery_spec.rb +29 -6
- data/spec/resource_spec.rb +30 -13
- data/spec/spec_helper.rb +1 -1
- data/spec/support/i18n.rb +1 -0
- data/spec/support/rspec.rb +1 -1
- data/spec/support/timecop.rb +1 -1
- data/spec/support/webmock.rb +1 -1
- metadata +8 -4
@@ -5,21 +5,18 @@ class Frenetic
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
module ClassMethods
|
8
|
-
def find(
|
9
|
-
params = { id:params } unless params.is_a?
|
8
|
+
def find(params)
|
9
|
+
params = { id:params } unless params.is_a?(Hash)
|
10
10
|
return as_mock(params) if test_mode?
|
11
|
-
|
12
|
-
|
13
|
-
end
|
11
|
+
response = api.get(member_url(params))
|
12
|
+
new(response.body) if response.success?
|
14
13
|
end
|
15
14
|
|
16
15
|
def all
|
17
16
|
return [] if test_mode?
|
18
|
-
|
19
|
-
|
20
|
-
Frenetic::ResourceCollection.new self, response.body
|
21
|
-
end
|
17
|
+
response = api.get(collection_url)
|
18
|
+
Frenetic::ResourceCollection.new(self, response.body) if response.success?
|
22
19
|
end
|
23
20
|
end
|
24
21
|
end
|
25
|
-
end
|
22
|
+
end
|
@@ -28,7 +28,7 @@ class Frenetic
|
|
28
28
|
def rebuild_structure!
|
29
29
|
destroy_structure!
|
30
30
|
@@signatures[struct_key] = signature
|
31
|
-
Struct.new(
|
31
|
+
Struct.new(struct_key, *@attrs.keys)
|
32
32
|
end
|
33
33
|
|
34
34
|
def structure_expired?
|
@@ -45,4 +45,4 @@ class Frenetic
|
|
45
45
|
Struct.send :remove_const, struct_key
|
46
46
|
end
|
47
47
|
end
|
48
|
-
end
|
48
|
+
end
|
data/lib/frenetic/connection.rb
CHANGED
@@ -29,15 +29,8 @@ class Frenetic
|
|
29
29
|
|
30
30
|
def process_config(raw_cfg)
|
31
31
|
@config = {}.merge(raw_cfg.to_hash)
|
32
|
-
@config[:url] =
|
33
|
-
cfgs = @config
|
34
|
-
if ConnectionConfigKeys.include?(k)
|
35
|
-
conf[:conn][k] = v
|
36
|
-
else
|
37
|
-
conf[:builder][k] = v
|
38
|
-
end
|
39
|
-
conf
|
40
|
-
end
|
32
|
+
@config[:url] = process_url_config(raw_cfg)
|
33
|
+
cfgs = process_config_options(@config)
|
41
34
|
[
|
42
35
|
@builder_config = cfgs[:builder],
|
43
36
|
@connection_config = cfgs[:conn]
|
@@ -66,8 +59,25 @@ class Frenetic
|
|
66
59
|
|
67
60
|
private
|
68
61
|
|
62
|
+
def process_url_config(raw_cfg)
|
63
|
+
url = Addressable::URI.parse(raw_cfg[:url])
|
64
|
+
return if !url
|
65
|
+
url.port = url.inferred_port if url.port.nil?
|
66
|
+
url
|
67
|
+
end
|
68
|
+
|
69
|
+
def process_config_options(options)
|
70
|
+
options.each_with_object(builder:{}, conn:{}) do |(k, v), conf|
|
71
|
+
if ConnectionConfigKeys.include?(k)
|
72
|
+
conf[:conn][k] = v
|
73
|
+
else
|
74
|
+
conf[:builder][k] = v
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
69
79
|
def validate_configuration!
|
70
|
-
|
80
|
+
fail ConfigError.new(self) if !valid?
|
71
81
|
end
|
72
82
|
|
73
83
|
def use_basic_auth(builder)
|
@@ -83,11 +93,9 @@ class Frenetic
|
|
83
93
|
builder.use(
|
84
94
|
FaradayMiddleware::RackCompatible,
|
85
95
|
Rack::Cache::Context,
|
86
|
-
{
|
87
|
-
|
88
|
-
|
89
|
-
ignore_headers: %w{Authorization Set-Cookie X-Content-Digest}
|
90
|
-
}
|
96
|
+
metastore: "file:tmp/rack/meta/#{cache_key}",
|
97
|
+
entitystore: "file:tmp/rack/body/#{cache_key}",
|
98
|
+
ignore_headers: %w(Authorization Set-Cookie X-Content-Digest)
|
91
99
|
)
|
92
100
|
end
|
93
101
|
|
@@ -104,7 +112,7 @@ class Frenetic
|
|
104
112
|
lib ? require(lib) : yield
|
105
113
|
rescue NameError, LoadError => err
|
106
114
|
context ||= self
|
107
|
-
raise
|
115
|
+
raise MissingDependency.new(lib, context, err)
|
108
116
|
end
|
109
117
|
end
|
110
|
-
end
|
118
|
+
end
|
data/lib/frenetic/errors.rb
CHANGED
@@ -24,9 +24,74 @@ class Frenetic
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
class MissingDependency < ConfigError
|
28
|
+
def initialize(lib, context, err)
|
29
|
+
@lib, @context, @err = lib, context, err
|
30
|
+
super(message)
|
31
|
+
end
|
32
|
+
|
33
|
+
def message
|
34
|
+
"Could not load required `#{@lib}` dependency for " \
|
35
|
+
"#{@context} (#{@err.class}: #{@err.message})"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
27
39
|
# Raised when there is a Hypermedia error
|
28
40
|
HypermediaError = Class.new(Error)
|
29
41
|
|
42
|
+
# Raised when there is no _link entry for the desired resource
|
43
|
+
class MissingRelevantLink < HypermediaError
|
44
|
+
def initialize(tmpl_vars, link_set)
|
45
|
+
@tmpl_vars = tmpl_vars
|
46
|
+
@link_set = link_set
|
47
|
+
end
|
48
|
+
|
49
|
+
def message
|
50
|
+
"Could not find a relevant link for the data provided.\n" \
|
51
|
+
"Are any of the links missing the templated:true property?\n" \
|
52
|
+
" Template Data: #{@tmpl_vars}\n" \
|
53
|
+
" Link Set: #{@link_set.collect(&:as_json)}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Raised when a Resource's GET Url is not included in the _links hash
|
58
|
+
class MissingResourceUrl < HypermediaError
|
59
|
+
def initialize(resource)
|
60
|
+
@resource = resource
|
61
|
+
super(message)
|
62
|
+
end
|
63
|
+
|
64
|
+
def message
|
65
|
+
%("No Hypermedia GET Url found for the resource "#{@resource}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Raised when there is no schema defined by the Api root for the given resource
|
70
|
+
class MissingSchemaDefinition < HypermediaError
|
71
|
+
def initialize(namespace)
|
72
|
+
@namespace = namespace
|
73
|
+
super(message)
|
74
|
+
end
|
75
|
+
|
76
|
+
def message
|
77
|
+
%(Could not find schema definition for the resource "#{@namespace}")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Raised when an expanded URL template is passed the wrong number of arguments
|
82
|
+
class UnfulfilledLinkTemplate < HypermediaError
|
83
|
+
def initialize(template, data)
|
84
|
+
@template = template
|
85
|
+
@data = data
|
86
|
+
end
|
87
|
+
|
88
|
+
def message
|
89
|
+
"The data provided could not satisfy the template requirements.\n" \
|
90
|
+
" Template: #{@template.pattern}\n" \
|
91
|
+
" Data: #{@data}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
30
95
|
# Raised when there is a Link Template error
|
31
96
|
LinkTemplateError = Class.new(Error)
|
32
97
|
|
@@ -52,7 +117,7 @@ class Frenetic
|
|
52
117
|
def message
|
53
118
|
"Mock resource not defined for `#{namespace}`." \
|
54
119
|
" Create a new class that inherits from `#{resource}` and mixin" \
|
55
|
-
|
120
|
+
' `Frenetic::ResourceMockery` to define a mock.'
|
56
121
|
end
|
57
122
|
end
|
58
123
|
|
@@ -109,4 +174,4 @@ class Frenetic
|
|
109
174
|
@original_exception.message
|
110
175
|
end
|
111
176
|
end
|
112
|
-
end
|
177
|
+
end
|
@@ -3,35 +3,33 @@ require 'active_support/core_ext/hash/indifferent_access'
|
|
3
3
|
|
4
4
|
class Frenetic
|
5
5
|
class HypermediaLink
|
6
|
-
def initialize(
|
6
|
+
def initialize(link)
|
7
7
|
@link = link.with_indifferent_access
|
8
8
|
end
|
9
9
|
|
10
|
-
def href(
|
10
|
+
def href(tmpl_data = {})
|
11
11
|
if templated?
|
12
|
-
expand
|
12
|
+
expand(tmpl_data)
|
13
13
|
else
|
14
14
|
@link['href']
|
15
15
|
end
|
16
16
|
end
|
17
|
-
|
17
|
+
alias_method :to_url, :href
|
18
18
|
|
19
19
|
def templated?
|
20
20
|
return false unless hash?
|
21
|
-
|
22
21
|
@link['templated'] == true
|
23
22
|
end
|
24
23
|
|
25
|
-
def expandable?(
|
24
|
+
def expandable?(tmpl_data)
|
26
25
|
return false unless templated?
|
27
|
-
|
28
26
|
tmpl_data = normalize_data(tmpl_data)
|
29
|
-
|
30
|
-
|
27
|
+
tmpl_dataset = template.variables & tmpl_data.keys.map(&:to_s)
|
28
|
+
tmpl_dataset.size == template.variables.size
|
31
29
|
end
|
32
30
|
|
33
31
|
def template
|
34
|
-
@template ||= Addressable::Template.new
|
32
|
+
@template ||= Addressable::Template.new(@link['href'])
|
35
33
|
end
|
36
34
|
|
37
35
|
def as_json
|
@@ -44,31 +42,26 @@ class Frenetic
|
|
44
42
|
|
45
43
|
private
|
46
44
|
|
47
|
-
def expand(
|
45
|
+
def expand(tmpl_data)
|
48
46
|
tmpl_data = normalize_data(tmpl_data)
|
49
|
-
|
50
|
-
|
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}"
|
47
|
+
return template.expand(tmpl_data).to_s if expandable?(tmpl_data)
|
48
|
+
fail UnfulfilledLinkTemplate.new(template, tmpl_data)
|
56
49
|
end
|
57
50
|
|
58
51
|
def hash?
|
59
|
-
@link.is_a?
|
52
|
+
@link.is_a?(Hash)
|
60
53
|
end
|
61
54
|
|
62
|
-
def normalize_data(
|
63
|
-
return data if data.is_a?
|
64
|
-
|
55
|
+
def normalize_data(data)
|
56
|
+
return data if data.is_a?(Hash)
|
65
57
|
infer_template_values data
|
66
58
|
end
|
67
59
|
|
68
|
-
def infer_template_values(
|
60
|
+
def infer_template_values(data)
|
69
61
|
key = template.variables.first
|
70
|
-
|
71
|
-
|
62
|
+
{
|
63
|
+
key => data
|
64
|
+
}
|
72
65
|
end
|
73
66
|
end
|
74
|
-
end
|
67
|
+
end
|
@@ -5,7 +5,7 @@ require 'frenetic/hypermedia_link'
|
|
5
5
|
|
6
6
|
class Frenetic
|
7
7
|
class HypermediaLinkSet < Delegator
|
8
|
-
def initialize(
|
8
|
+
def initialize(link_set = [])
|
9
9
|
link_set = [link_set] unless link_set.is_a? Array
|
10
10
|
|
11
11
|
@link_set = link_set.map do |link|
|
@@ -17,27 +17,24 @@ class Frenetic
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def href(
|
20
|
+
def href(tmpl_vars = {})
|
21
21
|
return @link_set.first.href if tmpl_vars.blank?
|
22
|
-
|
23
|
-
link
|
22
|
+
link = find_relevant_link(tmpl_vars)
|
23
|
+
link && link.href(tmpl_vars)
|
24
24
|
end
|
25
25
|
|
26
|
-
def [](
|
27
|
-
@link_set.find{ |link| link.rel == relation.to_s }
|
26
|
+
def [](relation)
|
27
|
+
@link_set.find { |link| link.rel == relation.to_s }
|
28
28
|
end
|
29
29
|
|
30
|
-
def find_relevant_link(
|
31
|
-
@link_set.find
|
32
|
-
|
33
|
-
|
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)}"
|
30
|
+
def find_relevant_link(tmpl_vars)
|
31
|
+
@link_set.find do |link|
|
32
|
+
link.expandable?(tmpl_vars)
|
33
|
+
end || fail(Frenetic::MissingRelevantLink.new(tmpl_vars, @link_set))
|
37
34
|
end
|
38
35
|
|
39
36
|
def __getobj__
|
40
37
|
@link_set
|
41
38
|
end
|
42
39
|
end
|
43
|
-
end
|
40
|
+
end
|
@@ -3,13 +3,12 @@ require 'faraday_middleware/response_middleware'
|
|
3
3
|
class Frenetic
|
4
4
|
module Middleware
|
5
5
|
class HalJson < FaradayMiddleware::ParseJson
|
6
|
-
|
7
6
|
def process_response(env)
|
8
7
|
super
|
9
8
|
|
10
9
|
case env[:status]
|
11
|
-
when 500...599 then
|
12
|
-
when 400...499 then
|
10
|
+
when 500...599 then fail ServerError.new(env)
|
11
|
+
when 400...499 then fail ClientError.new(env)
|
13
12
|
end
|
14
13
|
rescue Faraday::Error::ParsingError => err
|
15
14
|
case env[:status]
|
@@ -23,4 +22,4 @@ class Frenetic
|
|
23
22
|
end
|
24
23
|
|
25
24
|
Faraday::Response.register_middleware \
|
26
|
-
hal_json:
|
25
|
+
hal_json: -> { Frenetic::Middleware::HalJson }
|
data/lib/frenetic/resource.rb
CHANGED
@@ -13,7 +13,7 @@ class Frenetic
|
|
13
13
|
include HalLinked
|
14
14
|
include MemberRestMethods
|
15
15
|
|
16
|
-
def self.api_client(
|
16
|
+
def self.api_client(client = nil)
|
17
17
|
if client
|
18
18
|
@api_client = client
|
19
19
|
elsif block_given?
|
@@ -24,33 +24,37 @@ class Frenetic
|
|
24
24
|
@api_client
|
25
25
|
end
|
26
26
|
end
|
27
|
+
|
27
28
|
# Alias class method hack
|
28
|
-
def self.api
|
29
|
+
def self.api
|
30
|
+
api_client
|
31
|
+
end
|
29
32
|
|
30
|
-
def self.namespace(
|
33
|
+
def self.namespace(namespace = nil)
|
31
34
|
if namespace
|
32
35
|
@namespace = namespace.to_s
|
33
36
|
elsif @namespace
|
34
37
|
@namespace
|
35
38
|
else
|
36
|
-
@namespace =
|
39
|
+
@namespace = to_s.demodulize.underscore
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
40
43
|
def self.properties
|
41
44
|
return mock_class.default_attributes if test_mode?
|
42
|
-
(api.schema[namespace]||{})['properties']
|
45
|
+
props = (api.schema[namespace] || {})['properties']
|
46
|
+
props || fail(MissingSchemaDefinition.new(namespace))
|
43
47
|
end
|
44
48
|
|
45
49
|
def self.mock_class
|
46
|
-
@mock_class
|
50
|
+
@mock_class || fail(Frenetic::UndefinedResourceMock.new(namespace, self))
|
47
51
|
end
|
48
52
|
|
49
|
-
def self.as_mock(
|
53
|
+
def self.as_mock(params = {})
|
50
54
|
mock_class.new params
|
51
55
|
end
|
52
56
|
|
53
|
-
def initialize(
|
57
|
+
def initialize(p = {})
|
54
58
|
build_params p
|
55
59
|
@attrs = {}
|
56
60
|
|
@@ -66,11 +70,11 @@ class Frenetic
|
|
66
70
|
def api_client
|
67
71
|
self.class.api_client
|
68
72
|
end
|
69
|
-
|
73
|
+
alias_method :api, :api_client
|
70
74
|
|
71
75
|
def attributes
|
72
76
|
@attributes ||= begin
|
73
|
-
@structure.each_pair.each_with_object({}) do |(k,v), attrs|
|
77
|
+
@structure.each_pair.each_with_object({}) do |(k, v), attrs|
|
74
78
|
attrs[k.to_s] = v
|
75
79
|
end
|
76
80
|
end
|
@@ -80,14 +84,14 @@ class Frenetic
|
|
80
84
|
@structure
|
81
85
|
end
|
82
86
|
|
83
|
-
def __setobj__(
|
87
|
+
def __setobj__(obj)
|
84
88
|
@attributes = nil
|
85
89
|
|
86
90
|
@structure = obj
|
87
91
|
end
|
88
92
|
|
89
93
|
def inspect
|
90
|
-
attrs = attributes.collect do |k,v|
|
94
|
+
attrs = attributes.collect do |k, v|
|
91
95
|
val = v.is_a?(String) ? "\"#{v}\"" : v || 'nil'
|
92
96
|
"#{k}=#{val}"
|
93
97
|
end.join(' ')
|
@@ -99,35 +103,37 @@ class Frenetic
|
|
99
103
|
"#{k}=#{val}"
|
100
104
|
end.join(' ')
|
101
105
|
|
102
|
-
"#<#{self.class}:0x#{
|
106
|
+
"#<#{self.class}:0x#{format('%x', object_id)}" \
|
103
107
|
" #{attrs}" \
|
104
108
|
" #{ivars}" \
|
105
|
-
|
109
|
+
'>'
|
106
110
|
end
|
107
111
|
|
108
112
|
private
|
109
113
|
|
110
|
-
def build_params(
|
114
|
+
def build_params(p)
|
111
115
|
@params = (p || {}).with_indifferent_access
|
112
116
|
end
|
113
117
|
|
114
118
|
def extract_embedded_resources
|
115
119
|
class_namespace = self.class.to_s.deconstantize
|
116
|
-
|
117
|
-
@params.fetch('_embedded',{}).each do |k,v|
|
120
|
+
@params.fetch('_embedded', {}).each do |k, attrs|
|
118
121
|
class_name = "#{class_namespace}::#{k.classify}"
|
119
|
-
klass
|
120
|
-
|
121
|
-
|
122
|
-
|
122
|
+
klass = begin
|
123
|
+
class_name.constantize
|
124
|
+
rescue
|
125
|
+
OpenStruct
|
126
|
+
end
|
127
|
+
if self.class.test_mode? && klass.respond_to?(:as_mock)
|
128
|
+
@attrs[k] = klass.as_mock(attrs)
|
123
129
|
else
|
124
|
-
klass.new
|
130
|
+
@attrs[k] = klass.new(attrs)
|
125
131
|
end
|
126
132
|
end
|
127
133
|
end
|
128
134
|
|
129
135
|
def build_structure
|
130
|
-
@structure = structure.new(
|
136
|
+
@structure = structure.new(*@attrs.values)
|
131
137
|
end
|
132
138
|
|
133
139
|
def namespace
|
@@ -139,7 +145,7 @@ class Frenetic
|
|
139
145
|
end
|
140
146
|
|
141
147
|
def self.test_mode?
|
142
|
-
api_client.config.test_mode
|
148
|
+
!api_client || api_client.config.test_mode
|
143
149
|
end
|
144
150
|
end
|
145
|
-
end
|
151
|
+
end
|