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.
- data/.gitignore +1 -0
- data/.irbrc +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +0 -1
- data/Gemfile +1 -3
- data/README.md +138 -125
- data/frenetic.gemspec +5 -6
- data/lib/frenetic.rb +31 -43
- data/lib/frenetic/concerns/collection_rest_methods.rb +13 -0
- data/lib/frenetic/concerns/configurable.rb +59 -0
- data/lib/frenetic/concerns/hal_linked.rb +59 -0
- data/lib/frenetic/concerns/member_rest_methods.rb +15 -0
- data/lib/frenetic/concerns/structured.rb +53 -0
- data/lib/frenetic/configuration.rb +40 -76
- data/lib/frenetic/middleware/hal_json.rb +23 -0
- data/lib/frenetic/resource.rb +77 -62
- data/lib/frenetic/resource_collection.rb +46 -0
- data/lib/frenetic/version.rb +2 -2
- data/spec/concerns/configurable_spec.rb +50 -0
- data/spec/concerns/hal_linked_spec.rb +116 -0
- data/spec/concerns/member_rest_methods_spec.rb +41 -0
- data/spec/concerns/structured_spec.rb +214 -0
- data/spec/configuration_spec.rb +99 -0
- data/spec/fixtures/test_api_requests.rb +88 -0
- data/spec/frenetic_spec.rb +137 -0
- data/spec/middleware/hal_json_spec.rb +83 -0
- data/spec/resource_collection_spec.rb +80 -0
- data/spec/resource_spec.rb +211 -0
- data/spec/spec_helper.rb +4 -13
- data/spec/support/rspec.rb +5 -0
- data/spec/support/webmock.rb +3 -0
- metadata +59 -57
- data/.rvmrc +0 -1
- data/lib/frenetic/hal_json.rb +0 -23
- data/lib/frenetic/hal_json/response_wrapper.rb +0 -43
- data/lib/recursive_open_struct.rb +0 -79
- data/spec/fixtures/vcr_cassettes/description_error_unauthorized.yml +0 -36
- data/spec/fixtures/vcr_cassettes/description_success.yml +0 -38
- data/spec/lib/frenetic/configuration_spec.rb +0 -189
- data/spec/lib/frenetic/hal_json/response_wrapper_spec.rb +0 -70
- data/spec/lib/frenetic/hal_json_spec.rb +0 -68
- data/spec/lib/frenetic/resource_spec.rb +0 -182
- 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 '
|
2
|
-
require 'active_support/core_ext/
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
23
|
-
|
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
|
40
|
-
|
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
|
-
|
56
|
-
|
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
|
62
|
-
|
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
|
66
|
-
|
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
|
-
|
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
|
76
|
-
|
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
|
87
|
-
|
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
|
95
|
-
|
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 }
|
data/lib/frenetic/resource.rb
CHANGED
@@ -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
|
-
|
4
|
-
|
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
|
-
|
9
|
+
class Frenetic
|
10
|
+
class Resource < Delegator
|
11
|
+
include Structured
|
12
|
+
include HalLinked
|
13
|
+
include MemberRestMethods
|
14
14
|
|
15
|
-
|
15
|
+
def api
|
16
|
+
self.class.api
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
|
19
|
+
def self.api
|
20
|
+
api_client
|
20
21
|
end
|
21
22
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
57
|
-
|
58
|
-
next if key[0] == '_'
|
57
|
+
@structure = structure.new( *@attrs.values )
|
58
|
+
end
|
59
59
|
|
60
|
-
|
61
|
-
|
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
|
-
|
66
|
-
|
67
|
-
end
|
68
|
+
def __getobj__
|
69
|
+
@structure
|
68
70
|
end
|
69
71
|
|
70
|
-
def
|
71
|
-
|
72
|
+
def __setobj__
|
73
|
+
@structure
|
74
|
+
end
|
72
75
|
|
73
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
86
|
+
"#{k}=#{val}"
|
87
|
+
end.join(' ')
|
81
88
|
|
82
|
-
|
83
|
-
|
84
|
-
|
89
|
+
"#<#{self.class}:0x#{"%x" % self.object_id}" \
|
90
|
+
" #{attrs}" \
|
91
|
+
" #{ivars}" \
|
92
|
+
">"
|
93
|
+
end
|
85
94
|
|
86
|
-
|
87
|
-
|
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
|