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 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