frenetic 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/frenetic.gemspec +1 -1
- data/lib/frenetic.rb +8 -1
- data/lib/frenetic/concerns/hal_linked.rb +2 -2
- data/lib/frenetic/concerns/related.rb +54 -0
- data/lib/frenetic/concerns/structure_method_definer.rb +7 -0
- data/lib/frenetic/resource.rb +67 -35
- data/lib/frenetic/resource_collection.rb +4 -4
- data/lib/frenetic/resource_mockery.rb +7 -8
- data/lib/frenetic/structure_registry.rb +20 -0
- data/lib/frenetic/structure_registry/rebuilder.rb +41 -0
- data/lib/frenetic/structure_registry/retriever.rb +35 -0
- data/lib/frenetic/version.rb +1 -1
- data/spec/concerns/related_spec.rb +118 -0
- data/spec/concerns/structure_method_definer_spec.rb +24 -0
- data/spec/resource_mockery_spec.rb +14 -14
- data/spec/resource_spec.rb +63 -3
- data/spec/structure_registry/rebuilder_spec.rb +83 -0
- data/spec/structure_registry/retriever_spec.rb +119 -0
- data/spec/structure_registry_spec.rb +54 -0
- metadata +19 -7
- data/lib/frenetic/concerns/structured.rb +0 -48
- data/spec/concerns/structured_spec.rb +0 -210
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65214340421c9e57778a07e6c7f3d59770a8f310
|
4
|
+
data.tar.gz: 9bd8412c8fa0c265f203f737be2a93bb24263c4c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe042a279bed164972d0f50e1d9f4a71eb734817cb5bec09c56cce481f4c30d6f6a4cc135ad41061748bcb3dc4fe6398ece30aa51c277ff61c6569cd38b3e1e0
|
7
|
+
data.tar.gz: 35ed3d01a7d3be35e3a2452385ad50e9ceb0df61b61cbe92ad4e53dada9a4c50ecf8a149bd55b35600f43fb420204166c5ff9bbab704077ac8951567414b01ec
|
data/frenetic.gemspec
CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |gem|
|
|
22
22
|
gem.add_dependency 'addressable', '~> 2.3.4'
|
23
23
|
|
24
24
|
gem.add_development_dependency 'awesome_print'
|
25
|
-
gem.add_development_dependency 'rspec', '~> 3.
|
25
|
+
gem.add_development_dependency 'rspec', '~> 3.3.0'
|
26
26
|
gem.add_development_dependency 'rack-cache', '~> 1.2'
|
27
27
|
gem.add_development_dependency 'faraday-http-cache', '~> 0.4.2'
|
28
28
|
gem.add_development_dependency 'webmock', '~> 1.18.0'
|
data/lib/frenetic.rb
CHANGED
@@ -13,6 +13,8 @@ require 'frenetic/middleware/hal_json'
|
|
13
13
|
require 'frenetic/resource'
|
14
14
|
require 'frenetic/resource_collection'
|
15
15
|
|
16
|
+
require 'frenetic/structure_registry'
|
17
|
+
|
16
18
|
class Frenetic
|
17
19
|
extend Forwardable
|
18
20
|
include ActiveSupport::Configurable
|
@@ -33,6 +35,7 @@ class Frenetic
|
|
33
35
|
config_accessor :username
|
34
36
|
|
35
37
|
def_delegators :connection, :delete, :get, :head, :options, :patch, :post, :put
|
38
|
+
def_delegators :structure_registry, :construct
|
36
39
|
|
37
40
|
# Can't explicitly use config_accessor because we need defaults and
|
38
41
|
# ActiveSupport < 4 does not support them
|
@@ -85,7 +88,11 @@ class Frenetic
|
|
85
88
|
briefly_memoize :description
|
86
89
|
|
87
90
|
def schema
|
88
|
-
description
|
91
|
+
description.fetch('_embedded', {}).fetch('schema')
|
92
|
+
end
|
93
|
+
|
94
|
+
def structure_registry
|
95
|
+
@structure_registry ||= StructureRegistry.new
|
89
96
|
end
|
90
97
|
|
91
98
|
def reset_connection!
|
@@ -7,12 +7,12 @@ class Frenetic
|
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
9
|
def links
|
10
|
-
@
|
10
|
+
@raw_attributes.fetch('_links', {})
|
11
11
|
end
|
12
12
|
|
13
13
|
def member_url(params = {})
|
14
14
|
resource = @resource_type || self.class.to_s.demodulize.underscore
|
15
|
-
return self.class.member_url(params)
|
15
|
+
return self.class.member_url(params) if links.empty?
|
16
16
|
link = links[resource] || links['self']
|
17
17
|
fail MissingResourceUrl.new(resource) if !link
|
18
18
|
HypermediaLinkSet.new(link).href params
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
3
|
+
class Frenetic
|
4
|
+
module Related
|
5
|
+
extend Frenetic::StructureMethodDefiner
|
6
|
+
|
7
|
+
structure do |resource|
|
8
|
+
resource.send(:relations).each do |relation, props|
|
9
|
+
define_method(relation) do
|
10
|
+
resource.fetch_related_resource(relation, props)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_related_resources
|
16
|
+
links.each do |k, attrs|
|
17
|
+
next if k == 'self'
|
18
|
+
Array.wrap(attrs).each do |relation|
|
19
|
+
relations[k] = relation
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_related_resource(relation, props)
|
25
|
+
begin
|
26
|
+
response = api.get(props['href'])
|
27
|
+
rescue ClientParsingError, ClientError => ex
|
28
|
+
raise if ex.status != 404
|
29
|
+
raise ResourceNotFound.new(self, props)
|
30
|
+
end
|
31
|
+
return nil unless response.success?
|
32
|
+
resource_class = self.class.find_resource_class(relation)
|
33
|
+
if collection?(relation)
|
34
|
+
self.class.extract_embedded_resources(response.body).fetch(relation, [])
|
35
|
+
else
|
36
|
+
resource_class.new(response.body)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def collection?(relation)
|
43
|
+
relation != relation.singularize
|
44
|
+
end
|
45
|
+
|
46
|
+
def relations
|
47
|
+
@_relations ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
def schema
|
51
|
+
api.description.fetch('_embedded', {}).fetch('schema', {})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/frenetic/resource.rb
CHANGED
@@ -3,18 +3,21 @@ require 'ostruct'
|
|
3
3
|
require 'active_support/inflector'
|
4
4
|
require 'active_support/core_ext/hash/indifferent_access'
|
5
5
|
|
6
|
-
require 'frenetic/concerns/
|
6
|
+
require 'frenetic/concerns/structure_method_definer'
|
7
|
+
require 'frenetic/concerns/related'
|
7
8
|
require 'frenetic/concerns/hal_linked'
|
8
9
|
require 'frenetic/concerns/member_rest_methods'
|
9
10
|
require 'frenetic/concerns/persistence'
|
10
11
|
|
11
12
|
class Frenetic
|
12
13
|
class Resource < Delegator
|
13
|
-
include
|
14
|
+
include Related
|
14
15
|
include HalLinked
|
15
16
|
include MemberRestMethods
|
16
17
|
include Persistence
|
17
18
|
|
19
|
+
attr_reader :known_attributes, :raw_attributes
|
20
|
+
|
18
21
|
def self.api_client(client = nil)
|
19
22
|
if client
|
20
23
|
@api_client = client
|
@@ -32,6 +35,32 @@ class Frenetic
|
|
32
35
|
api_client
|
33
36
|
end
|
34
37
|
|
38
|
+
def self.extract_embedded_resources(resource)
|
39
|
+
resource.fetch('_embedded', {}).each_with_object({}) do |(resource_name, attrs), embeds|
|
40
|
+
resource_class = find_resource_class(resource_name)
|
41
|
+
if test_mode? && resource_class.respond_to?(:as_mock)
|
42
|
+
embeds[resource_name] = resource_class.as_mock(attrs)
|
43
|
+
elsif attrs.is_a?(Array)
|
44
|
+
embeds[resource_name] = attrs.map do |a|
|
45
|
+
resource_class.new(a)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
embeds[resource_name] = resource_class.new(attrs)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.find_resource_class(resource_name)
|
54
|
+
class_namespace = self.to_s.deconstantize
|
55
|
+
class_name = "#{class_namespace}::#{resource_name.classify}"
|
56
|
+
begin
|
57
|
+
class_name.constantize
|
58
|
+
rescue NameError => ex
|
59
|
+
raise if ex.message !~ /uninitialized constant/
|
60
|
+
OpenStruct
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
35
64
|
def self.namespace(namespace = nil)
|
36
65
|
if namespace
|
37
66
|
@namespace = namespace.to_s
|
@@ -52,20 +81,21 @@ class Frenetic
|
|
52
81
|
@mock_class || fail(Frenetic::UndefinedResourceMock.new(namespace, self))
|
53
82
|
end
|
54
83
|
|
55
|
-
def self.as_mock(
|
56
|
-
mock_class.new
|
84
|
+
def self.as_mock(attributes = {})
|
85
|
+
mock_class.new(attributes)
|
57
86
|
end
|
58
87
|
|
59
|
-
def initialize(
|
60
|
-
@
|
61
|
-
|
88
|
+
def initialize(attributes = nil)
|
89
|
+
@raw_attributes = {}
|
90
|
+
@known_attributes = {}
|
91
|
+
initialize_with(attributes || {})
|
62
92
|
end
|
63
93
|
|
64
|
-
def initialize_with(
|
65
|
-
|
66
|
-
assign_attributes(@params)
|
94
|
+
def initialize_with(attributes)
|
95
|
+
assign_attributes(attributes)
|
67
96
|
extract_embedded_resources
|
68
|
-
|
97
|
+
extract_related_resources
|
98
|
+
init_structure
|
69
99
|
end
|
70
100
|
|
71
101
|
def api_client
|
@@ -73,10 +103,16 @@ class Frenetic
|
|
73
103
|
end
|
74
104
|
alias_method :api, :api_client
|
75
105
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
106
|
+
def attributes=(attributes)
|
107
|
+
assign_attributes(attributes)
|
108
|
+
end
|
109
|
+
|
110
|
+
def assign_attributes(new_attributes)
|
111
|
+
if !new_attributes.respond_to?(:stringify_keys)
|
112
|
+
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
|
79
113
|
end
|
114
|
+
@raw_attributes.merge!(new_attributes.stringify_keys)
|
115
|
+
_assign_attributes(@raw_attributes)
|
80
116
|
end
|
81
117
|
|
82
118
|
def attributes
|
@@ -87,13 +123,16 @@ class Frenetic
|
|
87
123
|
end
|
88
124
|
end
|
89
125
|
|
126
|
+
def extract_embedded_resources
|
127
|
+
@known_attributes.merge!(self.class.extract_embedded_resources(@raw_attributes))
|
128
|
+
end
|
129
|
+
|
90
130
|
def __getobj__
|
91
131
|
@structure
|
92
132
|
end
|
93
133
|
|
94
134
|
def __setobj__(obj)
|
95
135
|
@attributes = nil
|
96
|
-
|
97
136
|
@structure = obj
|
98
137
|
end
|
99
138
|
|
@@ -103,7 +142,7 @@ class Frenetic
|
|
103
142
|
"#{k}=#{val}"
|
104
143
|
end.join(' ')
|
105
144
|
|
106
|
-
ivars = (instance_variables - [:@structure, :@attributes]).map do |k|
|
145
|
+
ivars = (instance_variables - [:@structure, :@attributes, :@known_attributes, :@raw_attributes, :@_relations]).map do |k|
|
107
146
|
val = instance_variable_get k
|
108
147
|
val = val.is_a?(String) ? "\"#{val}\"" : val || 'nil'
|
109
148
|
|
@@ -118,29 +157,22 @@ class Frenetic
|
|
118
157
|
|
119
158
|
private
|
120
159
|
|
121
|
-
def
|
122
|
-
|
160
|
+
def _assign_attributes(attributes)
|
161
|
+
properties.keys.each do |k|
|
162
|
+
_assign_attribute(k, attributes[k])
|
163
|
+
end
|
123
164
|
end
|
124
165
|
|
125
|
-
def
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
rescue
|
132
|
-
OpenStruct
|
133
|
-
end
|
134
|
-
if self.class.test_mode? && klass.respond_to?(:as_mock)
|
135
|
-
@attrs[k] = klass.as_mock(attrs)
|
136
|
-
else
|
137
|
-
@attrs[k] = klass.new(attrs)
|
138
|
-
end
|
139
|
-
end
|
166
|
+
def _assign_attribute(key, value)
|
167
|
+
@known_attributes[key] = value
|
168
|
+
end
|
169
|
+
|
170
|
+
def init_structure
|
171
|
+
@structure = api_client.construct(self, @known_attributes, struct_key)
|
140
172
|
end
|
141
173
|
|
142
|
-
def
|
143
|
-
|
174
|
+
def struct_key
|
175
|
+
"#{self.class}::FreneticResourceStruct".gsub('::', '')
|
144
176
|
end
|
145
177
|
|
146
178
|
def namespace
|
@@ -6,10 +6,10 @@ class Frenetic
|
|
6
6
|
include HalLinked
|
7
7
|
include CollectionRestMethods
|
8
8
|
|
9
|
-
def initialize(resource,
|
9
|
+
def initialize(resource, attributes = {})
|
10
10
|
@resource_class = resource
|
11
|
-
@resources
|
12
|
-
@
|
11
|
+
@resources = []
|
12
|
+
@raw_attributes = (attributes || {}).stringify_keys
|
13
13
|
|
14
14
|
extract_resources!
|
15
15
|
end
|
@@ -43,7 +43,7 @@ class Frenetic
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def embedded_collection
|
46
|
-
@
|
46
|
+
@raw_attributes.fetch('_embedded', {}).fetch(collection_key, [])
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|
@@ -8,7 +8,7 @@ class Frenetic
|
|
8
8
|
extend Forwardable
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
11
|
-
def_delegators :@
|
11
|
+
def_delegators :@raw_params, :as_json, :to_json
|
12
12
|
|
13
13
|
included do
|
14
14
|
# I'm sure this violates some sort of CS principle or best practice,
|
@@ -17,11 +17,11 @@ class Frenetic
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def attributes
|
20
|
-
@
|
20
|
+
@known_attributes
|
21
21
|
end
|
22
22
|
|
23
23
|
def properties
|
24
|
-
@
|
24
|
+
@known_attributes.each_with_object({}) do |(k, v), props|
|
25
25
|
props[k] = v.class.to_s.underscore
|
26
26
|
end
|
27
27
|
end
|
@@ -44,14 +44,13 @@ class Frenetic
|
|
44
44
|
|
45
45
|
private
|
46
46
|
|
47
|
-
def
|
48
|
-
raw_params = (params || {}).with_indifferent_access
|
47
|
+
def _assign_attributes(attributes)
|
49
48
|
defaults = default_attributes.with_indifferent_access
|
50
|
-
@
|
49
|
+
@known_attributes = cast_types(defaults.deep_merge(@raw_attributes))
|
51
50
|
end
|
52
51
|
|
53
|
-
def
|
54
|
-
@structure = OpenStruct.new(@
|
52
|
+
def init_structure
|
53
|
+
@structure = OpenStruct.new(@known_attributes)
|
55
54
|
end
|
56
55
|
|
57
56
|
# A naive attempt to cast the attribute types of the incoming mock data
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'frenetic/structure_registry/retriever'
|
2
|
+
|
3
|
+
class Frenetic
|
4
|
+
class StructureRegistry
|
5
|
+
attr_reader :signatures
|
6
|
+
|
7
|
+
def initialize(retriever_class: Frenetic::StructureRegistry::Retriever)
|
8
|
+
@signatures = {}
|
9
|
+
@retriever_class = retriever_class
|
10
|
+
end
|
11
|
+
|
12
|
+
def construct(resource, attributes, key)
|
13
|
+
fetch(resource, attributes, key).new(*attributes.values)
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch(resource, attributes, key)
|
17
|
+
@retriever_class.new(signatures, resource, attributes, key).call
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Frenetic
|
2
|
+
class StructureRegistry
|
3
|
+
class Rebuilder
|
4
|
+
attr_reader :signatures, :resource
|
5
|
+
|
6
|
+
def initialize(signatures, resource, attributes, key, signature)
|
7
|
+
@signatures, @resource, @attributes, @key, @signature = signatures, resource, attributes, key, signature
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
destroy!
|
12
|
+
signatures[@key] = @signature
|
13
|
+
Struct.new(@key, *@attributes.keys, &structure_instance_methods)
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroy!
|
17
|
+
return unless exists?
|
18
|
+
signatures.delete(@key)
|
19
|
+
Struct.send(:remove_const, @key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def exists?
|
23
|
+
Struct.constants.include?(@key.to_sym)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def structure_instance_methods
|
29
|
+
method_builders = resource.class.ancestors[1..-1].map do |ancestor|
|
30
|
+
ancestor.instance_variable_get('@_structure_block')
|
31
|
+
end.compact
|
32
|
+
_resource = resource
|
33
|
+
Proc.new do
|
34
|
+
method_builders.each do |builder|
|
35
|
+
instance_exec(_resource, &builder)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'frenetic/structure_registry/rebuilder'
|
2
|
+
|
3
|
+
class Frenetic
|
4
|
+
class StructureRegistry
|
5
|
+
class Retriever
|
6
|
+
def initialize(signatures, resource, attributes, key, rebuilder_class: Rebuilder)
|
7
|
+
if key.blank?
|
8
|
+
raise ArgumentError, "When registering a resource structure, you must provide a non-blank key"
|
9
|
+
end
|
10
|
+
@signatures, @resource, @attributes, @key = signatures, resource, attributes, key
|
11
|
+
@rebuilder_class = rebuilder_class
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
if expired?
|
16
|
+
@rebuilder_class.new(@signatures, @resource, @attributes, @key, struct_signature).call
|
17
|
+
else
|
18
|
+
fetch_structure
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def expired?
|
23
|
+
@signatures[@key] != struct_signature
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch_structure
|
27
|
+
Struct.const_get(@key)
|
28
|
+
end
|
29
|
+
|
30
|
+
def struct_signature
|
31
|
+
Digest::SHA1.hexdigest(@attributes.keys.sort.join(''))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|