frenetic 1.0.0.alpha.1 → 1.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.
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