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