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

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