frenetic 2.0.0 → 3.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/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
|