frenetic 0.0.12 → 0.0.20.alpha.0

Sign up to get free protection for your applications and to get access to all the features.
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