frenetic 0.0.20.alpha.6 → 1.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -1
  3. data/Appraisals +9 -0
  4. data/Gemfile +1 -1
  5. data/README.md +2 -2
  6. data/frenetic.gemspec +8 -5
  7. data/gemfiles/faraday_08.gemfile +10 -0
  8. data/gemfiles/faraday_08.gemfile.lock +77 -0
  9. data/gemfiles/faraday_09.gemfile +10 -0
  10. data/gemfiles/faraday_09.gemfile.lock +77 -0
  11. data/lib/frenetic.rb +57 -30
  12. data/lib/frenetic/briefly_memoizable.rb +34 -0
  13. data/lib/frenetic/concerns/collection_rest_methods.rb +1 -1
  14. data/lib/frenetic/concerns/hal_linked.rb +5 -35
  15. data/lib/frenetic/concerns/member_rest_methods.rb +0 -2
  16. data/lib/frenetic/concerns/structured.rb +0 -5
  17. data/lib/frenetic/connection.rb +110 -0
  18. data/lib/frenetic/errors.rb +112 -0
  19. data/lib/frenetic/hypermedia_link.rb +74 -0
  20. data/lib/frenetic/hypermedia_link_set.rb +43 -0
  21. data/lib/frenetic/middleware/hal_json.rb +9 -12
  22. data/lib/frenetic/resource.rb +22 -6
  23. data/lib/frenetic/resource_collection.rb +0 -1
  24. data/lib/frenetic/resource_mockery.rb +55 -1
  25. data/lib/frenetic/version.rb +1 -1
  26. data/spec/{concerns/breifly_memoizable_spec.rb → briefly_memoizable_spec.rb} +10 -18
  27. data/spec/concerns/hal_linked_spec.rb +49 -62
  28. data/spec/concerns/member_rest_methods_spec.rb +8 -10
  29. data/spec/concerns/structured_spec.rb +70 -75
  30. data/spec/connection_spec.rb +137 -0
  31. data/spec/fixtures/test_api_requests.rb +8 -2
  32. data/spec/frenetic_spec.rb +221 -133
  33. data/spec/hypermedia_link_set_spec.rb +155 -0
  34. data/spec/hypermedia_link_spec.rb +153 -0
  35. data/spec/middleware/hal_json_spec.rb +13 -15
  36. data/spec/resource_collection_spec.rb +17 -16
  37. data/spec/resource_mockery_spec.rb +69 -0
  38. data/spec/resource_spec.rb +110 -63
  39. data/spec/support/rspec.rb +0 -1
  40. metadata +88 -75
  41. data/lib/frenetic/concerns/briefly_memoizable.rb +0 -34
  42. data/lib/frenetic/concerns/configurable.rb +0 -59
  43. data/lib/frenetic/concerns/resource_mockery.rb +0 -48
  44. data/lib/frenetic/configuration.rb +0 -88
  45. data/spec/concerns/configurable_spec.rb +0 -50
  46. data/spec/concerns/resource_mockery_spec.rb +0 -56
  47. data/spec/configuration_spec.rb +0 -134
@@ -7,9 +7,7 @@ class Frenetic
7
7
  module ClassMethods
8
8
  def find( params )
9
9
  params = { id:params } unless params.is_a? Hash
10
-
11
10
  return as_mock(params) if test_mode?
12
-
13
11
  if response = api.get( member_url(params) ) and response.success?
14
12
  new response.body
15
13
  end
@@ -1,6 +1,5 @@
1
1
  class Frenetic
2
2
  module Structured
3
-
4
3
  # Stores the unique signature of each Resource Structure
5
4
  # Used to determine when a Structure has changed and thus
6
5
  # needs to be redefined.
@@ -28,9 +27,7 @@ class Frenetic
28
27
 
29
28
  def rebuild_structure!
30
29
  destroy_structure!
31
-
32
30
  @@signatures[struct_key] = signature
33
-
34
31
  Struct.new( struct_key, *@attrs.keys )
35
32
  end
36
33
 
@@ -44,9 +41,7 @@ class Frenetic
44
41
 
45
42
  def destroy_structure!
46
43
  return unless structure_defined?
47
-
48
44
  @@signatures.delete struct_key
49
-
50
45
  Struct.send :remove_const, struct_key
51
46
  end
52
47
  end
@@ -0,0 +1,110 @@
1
+ require 'delegate'
2
+
3
+ class Frenetic
4
+ class Connection < SimpleDelegator
5
+ attr_reader :builder_config, :connection_config, :errors
6
+
7
+ ConnectionConfigKeys = [:url, :params, :headers, :request, :ssl, :proxy]
8
+
9
+ def initialize(config = {})
10
+ @errors = {}
11
+ process_config(config)
12
+ validate_configuration!
13
+
14
+ connection = Faraday.new(connection_config) do |builder|
15
+ configure_authentication(builder)
16
+ configure_responder(builder)
17
+ configure_cache(builder)
18
+ configure_adapter(builder)
19
+ end
20
+ super(connection)
21
+ end
22
+
23
+ def valid?
24
+ @errors = {}
25
+ @errors[:adapter] = 'must be present' if !@config[:adapter]
26
+ @errors[:url] = 'must be present' if !@config[:url]
27
+ @errors.empty?
28
+ end
29
+
30
+ def process_config(raw_cfg)
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
41
+ [
42
+ @builder_config = cfgs[:builder],
43
+ @connection_config = cfgs[:conn]
44
+ ]
45
+ end
46
+
47
+ def configure_authentication(builder)
48
+ use_basic_auth(builder) if builder_config[:username]
49
+ use_token_auth(builder) if builder_config[:api_token]
50
+ end
51
+
52
+ def configure_responder(builder)
53
+ builder.response(:hal_json)
54
+ end
55
+
56
+ def configure_cache(builder)
57
+ case builder_config[:cache]
58
+ when :rack then use_rack_cache(builder)
59
+ when :rails then use_rails_cache(builder)
60
+ end
61
+ end
62
+
63
+ def configure_adapter(builder)
64
+ builder.adapter(builder_config[:adapter])
65
+ end
66
+
67
+ private
68
+
69
+ def validate_configuration!
70
+ raise ConfigError.new(self) if !valid?
71
+ end
72
+
73
+ def use_basic_auth(builder)
74
+ builder.request :basic_auth, builder_config[:username], builder_config[:password]
75
+ end
76
+
77
+ def use_token_auth(builder)
78
+ builder.request :token_auth, builder_config[:api_token]
79
+ end
80
+
81
+ def use_rack_cache(builder)
82
+ require_lib('rack-cache', 'Frenetic Rack::Cache caching strategy')
83
+ builder.use(
84
+ FaradayMiddleware::RackCompatible,
85
+ 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
+ }
91
+ )
92
+ end
93
+
94
+ def use_rails_cache(builder)
95
+ require_lib 'faraday-http-cache', 'Frenetic Rails caching strategy'
96
+ builder.use(Faraday::HttpCache, store:Rails.cache, logger:Rails.logger)
97
+ end
98
+
99
+ def cache_key
100
+ Digest::MD5.hexdigest connection_config[:url].hostname
101
+ end
102
+
103
+ def require_lib(lib = nil, context = nil)
104
+ lib ? require(lib) : yield
105
+ rescue NameError, LoadError => err
106
+ context ||= self
107
+ raise ConfigError, "Could not load required `#{lib}` dependency for #{context}: #{err.message}"
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,112 @@
1
+ require 'active_support/inflector'
2
+ require 'active_support/core_ext/array/conversions'
3
+
4
+ class Frenetic
5
+ # Generic Frenetic exception class.
6
+ Error = Class.new(StandardError)
7
+
8
+ # Raised when there is a configuration error
9
+ class ConfigError < Error
10
+ def initialize(model)
11
+ @model = model
12
+ super(message)
13
+ end
14
+
15
+ def message
16
+ if @model.is_a? String
17
+ @model
18
+ else
19
+ errs = @model.errors.collect do |key, msg|
20
+ "#{key.to_s.titleize} #{msg}"
21
+ end
22
+ "Invalid Configuration: #{errs.to_sentence}"
23
+ end
24
+ end
25
+ end
26
+
27
+ # Raised when there is a Hypermedia error
28
+ HypermediaError = Class.new(Error)
29
+
30
+ # Raised when there is a Link Template error
31
+ LinkTemplateError = Class.new(Error)
32
+
33
+ # Raised when a Resource does not have a mock class defined.
34
+ #
35
+ # For example:
36
+ #
37
+ # class Widget < Frenetic::Resource
38
+ # end
39
+ #
40
+ # class MockWidget < Widget
41
+ # include Frenetic::ResourceMockery
42
+ # end
43
+ #
44
+ # Would correctly create the necessary Mock Resource
45
+ class UndefinedResourceMock < Error
46
+ attr_reader :namespace, :resource
47
+ def initialize(namespace, resource)
48
+ @namespace = namespace
49
+ @resource = resource
50
+ end
51
+
52
+ def message
53
+ "Mock resource not defined for `#{namespace}`." \
54
+ " Create a new class that inherits from `#{resource}` and mixin" \
55
+ " `Frenetic::ResourceMockery` to define a mock."
56
+ end
57
+ end
58
+
59
+ # Parent class for all specific exceptions which are raised as a result of a
60
+ # network response.
61
+ class ResponseError < Error
62
+ attr_reader :env, :error, :method, :status, :url
63
+ def initialize(env)
64
+ env ||= {}
65
+ body = env.fetch(:body, {})
66
+ @env = env
67
+ @error = body['error']
68
+ @method = env[:method]
69
+ @status = env[:status]
70
+ @url = env[:url]
71
+ super(message)
72
+ end
73
+
74
+ def message
75
+ @error
76
+ end
77
+ end
78
+
79
+ # Raised when a network response returns a 400-level error
80
+ ClientError = Class.new(ResponseError)
81
+
82
+ # Raised when a network response returns a 500-level error
83
+ ServerError = Class.new(ResponseError)
84
+
85
+ # Parent class for all specific exceptions which are raised as a result of a
86
+ # parsing the network request response body.
87
+ class ParsingError < ResponseError
88
+ def message
89
+ "#{status} Error"
90
+ end
91
+ end
92
+
93
+ # Raised when there is a problem parsing the response body of a 400-level error
94
+ ClientParsingError = Class.new(ParsingError)
95
+
96
+ # Raised when there is a problem parsing the response body of a 400-level error
97
+ ServerParsingError = Class.new(ParsingError)
98
+
99
+ # Raised when there is a problem parsing the response body of an otherwise
100
+ # successful network request. Provides access to the original exception also.
101
+ class UnknownParsingError < ParsingError
102
+ attr_reader :original_exception
103
+ def initialize(env, err)
104
+ @original_exception = err
105
+ super(env)
106
+ end
107
+
108
+ def message
109
+ @original_exception.message
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,74 @@
1
+ require 'addressable/template'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+
4
+ class Frenetic
5
+ class HypermediaLink
6
+ def initialize( link )
7
+ @link = link.with_indifferent_access
8
+ end
9
+
10
+ def href( tmpl_data = {} )
11
+ if templated?
12
+ expand tmpl_data
13
+ else
14
+ @link['href']
15
+ end
16
+ end
17
+ alias :to_url :href
18
+
19
+ def templated?
20
+ return false unless hash?
21
+
22
+ @link['templated'] == true
23
+ end
24
+
25
+ def expandable?( tmpl_data )
26
+ return false unless templated?
27
+
28
+ tmpl_data = normalize_data(tmpl_data)
29
+
30
+ (template.variables & tmpl_data.keys.map(&:to_s)).size == template.variables.size
31
+ end
32
+
33
+ def template
34
+ @template ||= Addressable::Template.new @link['href']
35
+ end
36
+
37
+ def as_json
38
+ @link
39
+ end
40
+
41
+ def rel
42
+ @link['rel']
43
+ end
44
+
45
+ private
46
+
47
+ def expand( tmpl_data )
48
+ 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}"
56
+ end
57
+
58
+ def hash?
59
+ @link.is_a? Hash
60
+ end
61
+
62
+ def normalize_data( data )
63
+ return data if data.is_a? Hash
64
+
65
+ infer_template_values data
66
+ end
67
+
68
+ def infer_template_values( data )
69
+ key = template.variables.first
70
+
71
+ { key => data }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,43 @@
1
+ require 'delegate'
2
+ require 'active_support/core_ext/object/blank'
3
+
4
+ require 'frenetic/hypermedia_link'
5
+
6
+ class Frenetic
7
+ class HypermediaLinkSet < Delegator
8
+ def initialize( link_set = [] )
9
+ link_set = [link_set] unless link_set.is_a? Array
10
+
11
+ @link_set = link_set.map do |link|
12
+ if link.is_a? HypermediaLink
13
+ link
14
+ else
15
+ HypermediaLink.new link
16
+ end
17
+ end
18
+ end
19
+
20
+ def href( tmpl_vars = {} )
21
+ return @link_set.first.href if tmpl_vars.blank?
22
+
23
+ link = find_relevant_link( tmpl_vars ) and link.href( tmpl_vars )
24
+ end
25
+
26
+ def []( relation )
27
+ @link_set.find{ |link| link.rel == relation.to_s }
28
+ end
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)}"
37
+ end
38
+
39
+ def __getobj__
40
+ @link_set
41
+ end
42
+ end
43
+ end
@@ -7,23 +7,20 @@ class Frenetic
7
7
  def process_response(env)
8
8
  super
9
9
 
10
- if (500...599).include? env[:status]
11
- raise ServerError, env[:body]['error']
12
- elsif (400...499).include? env[:status]
13
- raise ClientError, env[:body]['error']
10
+ case env[:status]
11
+ when 500...599 then raise ServerError.new(env)
12
+ when 400...499 then raise ClientError.new(env)
14
13
  end
15
14
  rescue Faraday::Error::ParsingError => err
16
- if (500...599).include? env[:status]
17
- raise ServerError, "#{env[:status]} Error encountered"
18
- elsif (400...499).include? env[:status]
19
- raise ClientError, "#{env[:status]} Error encountered"
20
- else
21
- raise ParsingError, err.message
15
+ case env[:status]
16
+ when 500...599 then raise ServerParsingError.new(env)
17
+ when 400...499 then raise ClientParsingError.new(env)
18
+ else raise UnknownParsingError.new(env, err)
22
19
  end
23
20
  end
24
-
25
21
  end
26
22
  end
27
23
  end
28
24
 
29
- Faraday.register_middleware :response, hal_json:lambda { Frenetic::Middleware::HalJson }
25
+ Faraday::Response.register_middleware \
26
+ hal_json:lambda { Frenetic::Middleware::HalJson }
@@ -1,4 +1,5 @@
1
1
  require 'delegate'
2
+ require 'ostruct'
2
3
  require 'active_support/inflector'
3
4
  require 'active_support/core_ext/hash/indifferent_access'
4
5
 
@@ -37,14 +38,12 @@ class Frenetic
37
38
  end
38
39
 
39
40
  def self.properties
41
+ return mock_class.default_attributes if test_mode?
40
42
  (api.schema[namespace]||{})['properties'] or raise HypermediaError, %Q{Could not find schema definition for the resource "#{namespace}"}
41
43
  end
42
44
 
43
45
  def self.mock_class
44
- @mock_class or raise Frenetic::ClientError,
45
- "Mock resource not defined for #{namespace}." \
46
- " Subclass #{self} and mixin Frenetic::ResourceMockery" \
47
- " to define a mock"
46
+ @mock_class or raise Frenetic::UndefinedResourceMock.new(namespace, self)
48
47
  end
49
48
 
50
49
  def self.as_mock( params = {} )
@@ -53,12 +52,14 @@ class Frenetic
53
52
 
54
53
  def initialize( p = {} )
55
54
  build_params p
56
- @attrs = {}
55
+ @attrs = {}
57
56
 
58
57
  properties.keys.each do |k|
59
58
  @attrs[k] = @params[k]
60
59
  end
61
60
 
61
+ extract_embedded_resources
62
+
62
63
  build_structure
63
64
  end
64
65
 
@@ -91,7 +92,7 @@ class Frenetic
91
92
  "#{k}=#{val}"
92
93
  end.join(' ')
93
94
 
94
- ivars = (instance_variables - [:@structure]).map do |k|
95
+ ivars = (instance_variables - [:@structure, :@attributes]).map do |k|
95
96
  val = instance_variable_get k
96
97
  val = val.is_a?(String) ? "\"#{val}\"" : val || 'nil'
97
98
 
@@ -110,6 +111,21 @@ class Frenetic
110
111
  @params = (p || {}).with_indifferent_access
111
112
  end
112
113
 
114
+ def extract_embedded_resources
115
+ class_namespace = self.class.to_s.deconstantize
116
+
117
+ @params.fetch('_embedded',{}).each do |k,v|
118
+ 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
123
+ else
124
+ klass.new v
125
+ end
126
+ end
127
+ end
128
+
113
129
  def build_structure
114
130
  @structure = structure.new( *@attrs.values )
115
131
  end