frenetic 1.0.0.alpha.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +16 -0
  3. data/.gitignore +1 -1
  4. data/.irbrc +1 -1
  5. data/.rubocop.yml +45 -0
  6. data/.travis.yml +2 -2
  7. data/Appraisals +1 -1
  8. data/LICENSE +1 -1
  9. data/README.md +15 -15
  10. data/Rakefile +1 -1
  11. data/gemfiles/faraday_08.gemfile.lock +8 -8
  12. data/gemfiles/faraday_09.gemfile.lock +8 -8
  13. data/lib/frenetic.rb +13 -10
  14. data/lib/frenetic/briefly_memoizable.rb +9 -7
  15. data/lib/frenetic/concerns/collection_rest_methods.rb +4 -5
  16. data/lib/frenetic/concerns/hal_linked.rb +12 -9
  17. data/lib/frenetic/concerns/member_rest_methods.rb +7 -10
  18. data/lib/frenetic/concerns/structured.rb +2 -2
  19. data/lib/frenetic/connection.rb +25 -17
  20. data/lib/frenetic/errors.rb +67 -2
  21. data/lib/frenetic/hypermedia_link.rb +19 -26
  22. data/lib/frenetic/hypermedia_link_set.rb +11 -14
  23. data/lib/frenetic/middleware/hal_json.rb +3 -4
  24. data/lib/frenetic/resource.rb +31 -25
  25. data/lib/frenetic/resource_collection.rb +3 -3
  26. data/lib/frenetic/resource_mockery.rb +7 -5
  27. data/lib/frenetic/version.rb +2 -2
  28. data/spec/briefly_memoizable_spec.rb +1 -1
  29. data/spec/concerns/hal_linked_spec.rb +5 -5
  30. data/spec/concerns/member_rest_methods_spec.rb +1 -1
  31. data/spec/concerns/structured_spec.rb +6 -5
  32. data/spec/connection_spec.rb +16 -4
  33. data/spec/fixtures/test_api_requests.rb +32 -28
  34. data/spec/frenetic_spec.rb +3 -3
  35. data/spec/hypermedia_link_set_spec.rb +3 -3
  36. data/spec/hypermedia_link_spec.rb +1 -1
  37. data/spec/middleware/hal_json_spec.rb +3 -3
  38. data/spec/resource_collection_spec.rb +3 -4
  39. data/spec/resource_mockery_spec.rb +29 -6
  40. data/spec/resource_spec.rb +30 -13
  41. data/spec/spec_helper.rb +1 -1
  42. data/spec/support/i18n.rb +1 -0
  43. data/spec/support/rspec.rb +1 -1
  44. data/spec/support/timecop.rb +1 -1
  45. data/spec/support/webmock.rb +1 -1
  46. metadata +8 -4
@@ -5,21 +5,18 @@ class Frenetic
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  module ClassMethods
8
- def find( params )
9
- params = { id:params } unless params.is_a? Hash
8
+ def find(params)
9
+ params = { id:params } unless params.is_a?(Hash)
10
10
  return as_mock(params) if test_mode?
11
- if response = api.get( member_url(params) ) and response.success?
12
- new response.body
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
- if response = api.get( collection_url ) and response.success?
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( struct_key, *@attrs.keys )
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
@@ -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] = Addressable::URI.parse(raw_cfg[:url])
33
- cfgs = @config.inject({builder:{}, conn:{}}) do |conf, (k,v)|
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
- raise ConfigError.new(self) if !valid?
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
- metastore: "file:tmp/rack/meta/#{cache_key}",
88
- entitystore: "file:tmp/rack/body/#{cache_key}",
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 ConfigError, "Could not load required `#{lib}` dependency for #{context}: #{err.message}"
115
+ raise MissingDependency.new(lib, context, err)
108
116
  end
109
117
  end
110
- end
118
+ end
@@ -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
- " `Frenetic::ResourceMockery` to define a mock."
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( link )
6
+ def initialize(link)
7
7
  @link = link.with_indifferent_access
8
8
  end
9
9
 
10
- def href( tmpl_data = {} )
10
+ def href(tmpl_data = {})
11
11
  if templated?
12
- expand tmpl_data
12
+ expand(tmpl_data)
13
13
  else
14
14
  @link['href']
15
15
  end
16
16
  end
17
- alias :to_url :href
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?( tmpl_data )
24
+ def expandable?(tmpl_data)
26
25
  return false unless templated?
27
-
28
26
  tmpl_data = normalize_data(tmpl_data)
29
-
30
- (template.variables & tmpl_data.keys.map(&:to_s)).size == template.variables.size
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 @link['href']
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( tmpl_data )
45
+ def expand(tmpl_data)
48
46
  tmpl_data = normalize_data(tmpl_data)
49
-
50
- return template.expand( tmpl_data ).to_s if expandable? tmpl_data
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? Hash
52
+ @link.is_a?(Hash)
60
53
  end
61
54
 
62
- def normalize_data( data )
63
- return data if data.is_a? Hash
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( data )
60
+ def infer_template_values(data)
69
61
  key = template.variables.first
70
-
71
- { key => data }
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( link_set = [] )
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( tmpl_vars = {} )
20
+ def href(tmpl_vars = {})
21
21
  return @link_set.first.href if tmpl_vars.blank?
22
-
23
- link = find_relevant_link( tmpl_vars ) and link.href( tmpl_vars )
22
+ link = find_relevant_link(tmpl_vars)
23
+ link && link.href(tmpl_vars)
24
24
  end
25
25
 
26
- def []( relation )
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( tmpl_vars )
31
- @link_set.find{ |link| link.expandable? tmpl_vars } or
32
- raise Frenetic::HypermediaError,
33
- "Could not find a relevant link for the data provided.\n" \
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 raise ServerError.new(env)
12
- when 400...499 then raise ClientError.new(env)
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:lambda { Frenetic::Middleware::HalJson }
25
+ hal_json: -> { Frenetic::Middleware::HalJson }
@@ -13,7 +13,7 @@ class Frenetic
13
13
  include HalLinked
14
14
  include MemberRestMethods
15
15
 
16
- def self.api_client( client = nil )
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; api_client; end
29
+ def self.api
30
+ api_client
31
+ end
29
32
 
30
- def self.namespace( namespace = nil )
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 = self.to_s.demodulize.underscore
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'] or raise HypermediaError, %Q{Could not find schema definition for the resource "#{namespace}"}
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 or raise Frenetic::UndefinedResourceMock.new(namespace, self)
50
+ @mock_class || fail(Frenetic::UndefinedResourceMock.new(namespace, self))
47
51
  end
48
52
 
49
- def self.as_mock( params = {} )
53
+ def self.as_mock(params = {})
50
54
  mock_class.new params
51
55
  end
52
56
 
53
- def initialize( p = {} )
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
- alias :api :api_client
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__( obj )
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#{"%x" % self.object_id}" \
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( p )
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 = class_name.constantize rescue OpenStruct
120
-
121
- @attrs[k] = if self.class.test_mode? || is_a?(ResourceMockery)
122
- klass.as_mock v
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 v
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( *@attrs.values )
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