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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9feae0cbb4a8f08f8e7cc97d8417e0e3f1347778
4
- data.tar.gz: 71b2c0f9ced48a48dba39177a52e1fa7a0da6f43
3
+ metadata.gz: 65214340421c9e57778a07e6c7f3d59770a8f310
4
+ data.tar.gz: 9bd8412c8fa0c265f203f737be2a93bb24263c4c
5
5
  SHA512:
6
- metadata.gz: 9b54379136e8dbac6c6be19a6dfe75e21bc4f715c2197ca53a57b0743c005469c3d46c788fb839f94c79d386ce3ecbe971b47d6038f5bd44f6fb26f2b31ef515
7
- data.tar.gz: 5caddca24783caeb5d1f2a163b9a77c65b094a420d576f93f25e176ea22adb471548845934cf782163e0e11922b8840022be5f1146867bbd88d375dd9e6336a1
6
+ metadata.gz: fe042a279bed164972d0f50e1d9f4a71eb734817cb5bec09c56cce481f4c30d6f6a4cc135ad41061748bcb3dc4fe6398ece30aa51c277ff61c6569cd38b3e1e0
7
+ data.tar.gz: 35ed3d01a7d3be35e3a2452385ad50e9ceb0df61b61cbe92ad4e53dada9a4c50ecf8a149bd55b35600f43fb420204166c5ff9bbab704077ac8951567414b01ec
@@ -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.0.0'
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'
@@ -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['_embedded']['schema']
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
- @params['_links']
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) unless links.is_a?(Hash)
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
@@ -0,0 +1,7 @@
1
+ class Frenetic
2
+ module StructureMethodDefiner
3
+ def structure
4
+ @_structure_block = Proc.new if block_given?
5
+ end
6
+ end
7
+ end
@@ -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/structured'
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 Structured
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(params = {})
56
- mock_class.new params
84
+ def self.as_mock(attributes = {})
85
+ mock_class.new(attributes)
57
86
  end
58
87
 
59
- def initialize(params = {})
60
- @attrs = {}
61
- initialize_with(params)
88
+ def initialize(attributes = nil)
89
+ @raw_attributes = {}
90
+ @known_attributes = {}
91
+ initialize_with(attributes || {})
62
92
  end
63
93
 
64
- def initialize_with(p)
65
- build_params(p)
66
- assign_attributes(@params)
94
+ def initialize_with(attributes)
95
+ assign_attributes(attributes)
67
96
  extract_embedded_resources
68
- build_structure
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 assign_attributes(params)
77
- properties.keys.each do |k|
78
- @attrs[k] = params[k]
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 build_params(p)
122
- @params = (p || {}).with_indifferent_access
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 extract_embedded_resources
126
- class_namespace = self.class.to_s.deconstantize
127
- @params.fetch('_embedded', {}).each do |k, attrs|
128
- class_name = "#{class_namespace}::#{k.classify}"
129
- klass = begin
130
- class_name.constantize
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 build_structure
143
- @structure = structure.new(*@attrs.values)
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, params = {})
9
+ def initialize(resource, attributes = {})
10
10
  @resource_class = resource
11
- @resources = []
12
- @params = params || {}
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
- @params.fetch('_embedded', {}).fetch(collection_key, [])
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 :@params, :as_json, :to_json
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
- @params
20
+ @known_attributes
21
21
  end
22
22
 
23
23
  def properties
24
- @params.each_with_object({}) do |(k, v), props|
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 build_params(params)
48
- raw_params = (params || {}).with_indifferent_access
47
+ def _assign_attributes(attributes)
49
48
  defaults = default_attributes.with_indifferent_access
50
- @params = cast_types(defaults.deep_merge(raw_params))
49
+ @known_attributes = cast_types(defaults.deep_merge(@raw_attributes))
51
50
  end
52
51
 
53
- def build_structure
54
- @structure = OpenStruct.new(@attrs)
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