frenetic 0.0.12 → 0.0.20.alpha.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 (43) hide show
  1. data/.gitignore +1 -0
  2. data/.irbrc +3 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +0 -1
  5. data/Gemfile +1 -3
  6. data/README.md +138 -125
  7. data/frenetic.gemspec +5 -6
  8. data/lib/frenetic.rb +31 -43
  9. data/lib/frenetic/concerns/collection_rest_methods.rb +13 -0
  10. data/lib/frenetic/concerns/configurable.rb +59 -0
  11. data/lib/frenetic/concerns/hal_linked.rb +59 -0
  12. data/lib/frenetic/concerns/member_rest_methods.rb +15 -0
  13. data/lib/frenetic/concerns/structured.rb +53 -0
  14. data/lib/frenetic/configuration.rb +40 -76
  15. data/lib/frenetic/middleware/hal_json.rb +23 -0
  16. data/lib/frenetic/resource.rb +77 -62
  17. data/lib/frenetic/resource_collection.rb +46 -0
  18. data/lib/frenetic/version.rb +2 -2
  19. data/spec/concerns/configurable_spec.rb +50 -0
  20. data/spec/concerns/hal_linked_spec.rb +116 -0
  21. data/spec/concerns/member_rest_methods_spec.rb +41 -0
  22. data/spec/concerns/structured_spec.rb +214 -0
  23. data/spec/configuration_spec.rb +99 -0
  24. data/spec/fixtures/test_api_requests.rb +88 -0
  25. data/spec/frenetic_spec.rb +137 -0
  26. data/spec/middleware/hal_json_spec.rb +83 -0
  27. data/spec/resource_collection_spec.rb +80 -0
  28. data/spec/resource_spec.rb +211 -0
  29. data/spec/spec_helper.rb +4 -13
  30. data/spec/support/rspec.rb +5 -0
  31. data/spec/support/webmock.rb +3 -0
  32. metadata +59 -57
  33. data/.rvmrc +0 -1
  34. data/lib/frenetic/hal_json.rb +0 -23
  35. data/lib/frenetic/hal_json/response_wrapper.rb +0 -43
  36. data/lib/recursive_open_struct.rb +0 -79
  37. data/spec/fixtures/vcr_cassettes/description_error_unauthorized.yml +0 -36
  38. data/spec/fixtures/vcr_cassettes/description_success.yml +0 -38
  39. data/spec/lib/frenetic/configuration_spec.rb +0 -189
  40. data/spec/lib/frenetic/hal_json/response_wrapper_spec.rb +0 -70
  41. data/spec/lib/frenetic/hal_json_spec.rb +0 -68
  42. data/spec/lib/frenetic/resource_spec.rb +0 -182
  43. data/spec/lib/frenetic_spec.rb +0 -129
@@ -0,0 +1,13 @@
1
+ require 'active_support/concern'
2
+
3
+ class Frenetic
4
+ module CollectionRestMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ def get( id )
8
+ if response = api.get( member_url(id) ) and response.success?
9
+ @resource_class.new response.body
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_support/configurable'
2
+ require 'active_support/concern'
3
+
4
+ require 'frenetic/configuration'
5
+
6
+ class Frenetic
7
+ module Configurable
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include ActiveSupport::Configurable
12
+ # Don't allow the class to be configured
13
+ class << self
14
+ undef :configure
15
+ end
16
+ end
17
+
18
+ def initialize( cfg = {} )
19
+ config.merge! Frenetic::Configuration.new(cfg).attributes
20
+
21
+ @builder_config = Proc.new if block_given?
22
+ end
23
+
24
+ def configure
25
+ yield config
26
+ end
27
+
28
+ private
29
+
30
+ def validate_configuration!
31
+ raise( ConfigError, 'A URL must be defined' ) unless config.url
32
+ end
33
+
34
+ def configure_authentication( builder )
35
+ if config.username
36
+ builder.request :basic_auth, config.username, config.password
37
+ end
38
+
39
+ if config.api_token
40
+ builder.request :token_auth, config.api_token
41
+ end
42
+ end
43
+
44
+ def configure_caching( builder )
45
+ if config.cache[:metastore]
46
+ dependency 'rack-cache'
47
+
48
+ builder.use FaradayMiddleware::RackCompatible, Rack::Cache::Context, config.cache
49
+ end
50
+ end
51
+
52
+ def dependency( lib = nil )
53
+ lib ? require(lib) : yield
54
+ rescue NameError, LoadError => err
55
+ raise ConfigError, "Missing dependency for #{self}: #{err.message}"
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_support/concern'
2
+ require 'addressable/template'
3
+
4
+ class Frenetic
5
+ module HalLinked
6
+ extend ActiveSupport::Concern
7
+
8
+ def links
9
+ @params['_links']
10
+ end
11
+
12
+ def member_url( params = {} )
13
+ resource = @resource_type || self.class.to_s.demodulize.underscore
14
+
15
+ link = links[resource] || links['self'] or raise HypermediaError, %Q{No Hypermedia GET Url found for the resource "#{resource}"}
16
+
17
+ self.class.parse_link link, params
18
+ end
19
+
20
+ module ClassMethods
21
+ def links
22
+ api.description['_links']
23
+ end
24
+
25
+ def member_url( params = {} )
26
+ link = links[namespace] or raise HypermediaError, %Q{No Hypermedia GET Url found for the resource "#{namespace}"}
27
+
28
+ parse_link link, params
29
+ end
30
+
31
+ def parse_link( link, params = {} )
32
+ if link['templated']
33
+ expand_link link, params
34
+ else
35
+ link['href']
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def expand_link( link, params )
42
+ tmpl = Addressable::Template.new link['href']
43
+
44
+ if params && !params.is_a?(Hash)
45
+ params = infer_url_template_values tmpl, params
46
+ end
47
+
48
+ tmpl.expand( params ).to_s
49
+ end
50
+
51
+ def infer_url_template_values( tmpl, params )
52
+ key = tmpl.variables.first
53
+
54
+ { key => params }
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support/concern'
2
+
3
+ class Frenetic
4
+ module MemberRestMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def find( id = nil )
9
+ if response = api.get( member_url(id) ) and response.success?
10
+ new response.body
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ class Frenetic
2
+ module Structured
3
+
4
+ # Stores the unique signature of each Resource Structure
5
+ # Used to determine when a Structure has changed and thus
6
+ # needs to be redefined.
7
+ @@signatures = {}
8
+
9
+ def struct_key
10
+ "#{self.class}::FreneticResourceStruct".gsub '::', ''
11
+ end
12
+
13
+ def signature
14
+ @attrs.keys.sort.join('')
15
+ end
16
+
17
+ def structure
18
+ if structure_expired?
19
+ rebuild_structure!
20
+ else
21
+ fetch_structure
22
+ end
23
+ end
24
+
25
+ def fetch_structure
26
+ Struct.const_get struct_key
27
+ end
28
+
29
+ def rebuild_structure!
30
+ destroy_structure!
31
+
32
+ @@signatures[struct_key] = signature
33
+
34
+ Struct.new( struct_key, *@attrs.keys )
35
+ end
36
+
37
+ def structure_expired?
38
+ @@signatures[struct_key] != signature
39
+ end
40
+
41
+ def structure_defined?
42
+ Struct.constants.include? struct_key.to_sym
43
+ end
44
+
45
+ def destroy_structure!
46
+ return unless structure_defined?
47
+
48
+ @@signatures.delete struct_key
49
+
50
+ Struct.send :remove_const, struct_key
51
+ end
52
+ end
53
+ end
@@ -1,102 +1,66 @@
1
- require 'socket'
2
- require 'active_support/core_ext/object/blank'
3
- require 'active_support/core_ext/hash/keys'
4
- require 'active_support/core_ext/hash/deep_merge'
1
+ require 'addressable/uri'
2
+ require 'active_support/core_ext/hash/indifferent_access'
5
3
 
6
4
  class Frenetic
7
5
  class Configuration
8
6
 
9
7
  @@defaults = {
10
- adapter: nil,
11
- cache: nil,
12
- url: nil,
13
- username: nil,
14
- password: nil,
15
- headers: {
16
- accept: 'application/hal+json'
17
- },
18
- request: {},
19
- response: {}
8
+ headers: {
9
+ accept: 'application/hal+json',
10
+ user_agent: "Frenetic v#{Frenetic::VERSION}; #{Socket.gethostname}"
11
+ }
20
12
  }
21
13
 
22
- attr_accessor :adapter, :cache, :url, :username, :password
23
- attr_accessor :headers, :request, :response, :middleware
24
-
25
- def initialize( config = {} )
26
- config = @@defaults.deep_merge( config.symbolize_keys )
27
-
28
- map_api_key_to_username config
29
- append_user_agent config
30
- filter_cache_headers config
31
-
32
- config.each do |k, v|
33
- v.symbolize_keys! if v.is_a? Hash
34
-
35
- instance_variable_set "@#{k}", v
36
- end
14
+ def initialize( cfg = {} )
15
+ @_cfg = cfg.symbolize_keys
37
16
  end
38
17
 
39
- def attributes
40
- validate!
41
-
42
- (instance_variables - [:@middleware]).each_with_object({}) do |k, attrs|
43
- key = k.to_s.gsub( '@', '' )
44
-
45
- value = instance_variable_get( k )
46
-
47
- attrs[key.to_sym] = value
48
- end
18
+ def adapter
19
+ @_cfg[:adapter] || :net_http
49
20
  end
50
- alias_method :to_hash, :attributes
51
-
52
- def validate!
53
- raise(Frenetic::ConfigurationError, 'No API URL defined!') unless @url.present?
54
21
 
55
- if @cache
56
- raise( ConfigurationError, 'No cache :metastore defined!' ) unless @cache[:metastore].present?
57
- raise( ConfigurationError, "No cache :entitystore defined!" ) unless @cache[:entitystore].present?
58
- end
22
+ def api_token
23
+ @_cfg[:api_token]
59
24
  end
60
25
 
61
- def middleware
62
- @middleware ||= []
26
+ def attributes
27
+ {
28
+ adapter: adapter,
29
+ api_token: api_token,
30
+ cache: cache,
31
+ headers: headers,
32
+ password: password,
33
+ url: url,
34
+ username: username
35
+ }
63
36
  end
64
37
 
65
- def use( *args )
66
- middleware << args
38
+ def cache
39
+ if @_cfg[:cache] == :rack
40
+ {
41
+ metastore: 'file:tmp/rack/meta',
42
+ entitystore: 'file:tmp/rack/body',
43
+ ignore_headers: %w{Authorization Set-Cookie X-Content-Digest}
44
+ }
45
+ else
46
+ {}
47
+ end
67
48
  end
68
49
 
69
- private
70
-
71
- def user_agent
72
- "Frenetic v#{Frenetic::VERSION}; #{Socket.gethostname}"
50
+ def headers
51
+ @@defaults[:headers].merge( @_cfg[:headers] || {} )
73
52
  end
74
53
 
75
- def map_api_key_to_username( config )
76
- if config[:api_key]
77
- if config[:app_id]
78
- config[:username] = config.delete :app_id
79
- config[:password] = config.delete :api_key
80
- else
81
- config[:username] = config.delete :api_key
82
- end
83
- end
54
+ def password
55
+ @_cfg[:password] || @_cfg[:api_key]
84
56
  end
85
57
 
86
- def append_user_agent( config )
87
- if config[:headers][:user_agent]
88
- config[:headers][:user_agent] << " (#{user_agent})"
89
- else
90
- config[:headers][:user_agent] = user_agent
91
- end
58
+ def url
59
+ Addressable::URI.parse @_cfg[:url]
92
60
  end
93
61
 
94
- def filter_cache_headers( config )
95
- if config[:cache]
96
- ignore_headers = config[:cache][:ignore_headers] || []
97
-
98
- config[:cache][:ignore_headers] = (ignore_headers + %w[Set-Cookie X-Content-Digest]).uniq
99
- end
62
+ def username
63
+ @_cfg[:username] || @_cfg[:app_id]
100
64
  end
101
65
 
102
66
  end
@@ -0,0 +1,23 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ class Frenetic
4
+ module Middleware
5
+ class HalJson < FaradayMiddleware::ParseJson
6
+
7
+ def process_response(env)
8
+ super
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']
14
+ end
15
+ rescue Faraday::Error::ParsingError => err
16
+ raise ParsingError, err.message
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+
23
+ Faraday.register_middleware :response, hal_json:lambda { Frenetic::Middleware::HalJson }
@@ -1,90 +1,105 @@
1
+ require 'delegate'
1
2
  require 'active_support/inflector'
3
+ require 'active_support/core_ext/hash/indifferent_access'
2
4
 
3
- class Frenetic
4
- class Resource
5
-
6
- def initialize( attributes = {} )
7
- self.class.apply_schema
8
-
9
- attributes.each do |key, val|
10
- instance_variable_set "@#{key}", val
11
- end
5
+ require 'frenetic/concerns/structured'
6
+ require 'frenetic/concerns/hal_linked'
7
+ require 'frenetic/concerns/member_rest_methods'
12
8
 
13
- @_links ||= {}
9
+ class Frenetic
10
+ class Resource < Delegator
11
+ include Structured
12
+ include HalLinked
13
+ include MemberRestMethods
14
14
 
15
- build_associations attributes
15
+ def api
16
+ self.class.api
16
17
  end
17
18
 
18
- def links
19
- @_links
19
+ def self.api
20
+ api_client
20
21
  end
21
22
 
22
- def attributes
23
- self.class.schema.keys.each_with_object({}) do |key, attrs|
24
- attrs[key] = public_send key
23
+ def self.api_client( client = nil )
24
+ if client
25
+ @api_client = client
26
+ elsif block_given?
27
+ @api_client = Proc.new
28
+ elsif @api_client.is_a? Proc
29
+ @api_client.call
30
+ else
31
+ @api_client
25
32
  end
26
33
  end
27
- alias_method :to_hash, :attributes
28
-
29
- # Attempts to retrieve the Resource Schema from the API based on the name
30
- # of the subclass.
31
- class << self
32
- def api_client( client = nil )
33
- metaclass.instance_eval do
34
- define_method :api do
35
- block_given? ? yield : client
36
- end
37
- end
34
+
35
+ def self.namespace( namespace = nil )
36
+ if namespace
37
+ @namespace = namespace.to_s
38
+ elsif @namespace
39
+ @namespace
40
+ else
41
+ @namespace = self.to_s.demodulize.underscore
38
42
  end
43
+ end
39
44
 
40
- def schema
41
- if self.respond_to? :api
42
- class_name = self.to_s.demodulize.underscore
43
-
44
- if class_schema = api.description.resources.schema.send(class_name)
45
- class_schema.properties
46
- else
47
- {}
48
- end
49
- else
50
- raise MissingAPIReference,
51
- "This Resource needs a class accessor defined as " \
52
- "`.api` that references an instance of Frenetic."
53
- end
45
+ def self.properties
46
+ (api.schema[namespace]||{})['properties'] or raise HypermediaError, %Q{Could not find schema definition for the resource "#{namespace}"}
47
+ end
48
+
49
+ def initialize( p = {} )
50
+ @params = (p || {}).with_indifferent_access
51
+ @attrs = {}
52
+
53
+ properties.keys.each do |k|
54
+ @attrs[k] = @params[k]
54
55
  end
55
56
 
56
- def apply_schema
57
- schema.keys.each do |key|
58
- next if key[0] == '_'
57
+ @structure = structure.new( *@attrs.values )
58
+ end
59
59
 
60
- class_eval { attr_reader key.to_sym } unless instance_methods.include? key
61
- class_eval { attr_writer key.to_sym } unless instance_methods.include? "#{key}="
60
+ def attributes
61
+ @attributes ||= begin
62
+ @structure.each_pair.each_with_object({}) do |(k,v), attrs|
63
+ attrs[k.to_s] = v
62
64
  end
63
65
  end
66
+ end
64
67
 
65
- def metaclass
66
- class << self; self; end
67
- end
68
+ def __getobj__
69
+ @structure
68
70
  end
69
71
 
70
- def build_associations( attributes )
71
- return unless attributes['_embedded']
72
+ def __setobj__
73
+ @structure
74
+ end
72
75
 
73
- namespace = self.class.to_s.deconstantize
76
+ def inspect
77
+ attrs = @structure.each_pair.collect do |k,v|
78
+ val = v.is_a?(String) ? "\"#{v}\"" : v || 'nil'
79
+ "#{k}=#{val}"
80
+ end.join(' ')
74
81
 
75
- attributes['_embedded'].each do |key, value|
76
- self.class.class_eval do
77
- attr_reader key.to_sym
78
- end
82
+ ivars = (instance_variables - [:@structure]).map do |k|
83
+ val = instance_variable_get k
84
+ val = val.is_a?(String) ? "\"#{val}\"" : val || 'nil'
79
85
 
80
- assoc_class = "#{namespace}::#{key.classify}"
86
+ "#{k}=#{val}"
87
+ end.join(' ')
81
88
 
82
- if assoc_class = (assoc_class.constantize rescue nil)
83
- value = assoc_class.new value
84
- end
89
+ "#<#{self.class}:0x#{"%x" % self.object_id}" \
90
+ " #{attrs}" \
91
+ " #{ivars}" \
92
+ ">"
93
+ end
85
94
 
86
- instance_variable_set "@#{key}", value
87
- end
95
+ private
96
+
97
+ def namespace
98
+ self.class.namespace
99
+ end
100
+
101
+ def properties
102
+ self.class.properties
88
103
  end
89
104
 
90
105
  end