aptible-resource 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rubocop.yml +22 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.md +22 -0
- data/README.md +52 -0
- data/Rakefile +4 -0
- data/aptible-resource.gemspec +35 -0
- data/lib/aptible/resource.rb +7 -0
- data/lib/aptible/resource/adapter.rb +19 -0
- data/lib/aptible/resource/base.rb +70 -0
- data/lib/aptible/resource/model.rb +115 -0
- data/lib/aptible/resource/token.rb +10 -0
- data/lib/aptible/resource/version.rb +5 -0
- data/lib/hyper_resource.rb +302 -0
- data/lib/hyper_resource/adapter.rb +31 -0
- data/lib/hyper_resource/adapter/hal_json.rb +135 -0
- data/lib/hyper_resource/attributes.rb +100 -0
- data/lib/hyper_resource/exceptions.rb +40 -0
- data/lib/hyper_resource/link.rb +59 -0
- data/lib/hyper_resource/links.rb +63 -0
- data/lib/hyper_resource/modules/http.rb +131 -0
- data/lib/hyper_resource/modules/internal_attributes.rb +90 -0
- data/lib/hyper_resource/objects.rb +60 -0
- data/lib/hyper_resource/response.rb +2 -0
- data/lib/hyper_resource/version.rb +4 -0
- data/spec/aptible/resource/base_spec.rb +36 -0
- data/spec/aptible/resource/model_spec.rb +54 -0
- data/spec/fixtures/api.rb +11 -0
- data/spec/fixtures/mainframe.rb +5 -0
- data/spec/fixtures/token.rb +5 -0
- data/spec/shared/set_env.rb +10 -0
- data/spec/spec_helper.rb +14 -0
- metadata +225 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 953b5ea9c2b350a3b37e7bc3b1125fd1962e7628
|
4
|
+
data.tar.gz: 01317c3f36bd2061be1a95e95b1995f1c0d096d9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7015d0822a8d50f48248610da1f89a894e5228bbd8a5618b84a202ccb96170b9af8f866b343bd94c27ea22b9841b63f8ff07078d40343746794869ba9ba7ab68
|
7
|
+
data.tar.gz: cf190060c67bb26f41340340de34ec0620c6887e793e14633e49386ae1a7cb7978249dbf2656f79b03076de1519510a6db58359ade13fa1ce929f7c7be0f2aef
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Documentation:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
Encoding:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
DoubleNegation:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
AllCops:
|
11
|
+
Include:
|
12
|
+
- !ruby/regexp /\.rb$/
|
13
|
+
- !ruby/regexp /\.rake$/
|
14
|
+
- !ruby/regexp /\.gemspec$/
|
15
|
+
- !ruby/regexp /Rakefile$/
|
16
|
+
Exclude:
|
17
|
+
# Exclude line length check from autogenerated files
|
18
|
+
- !ruby/regexp /\/db\/schema\.rb$/
|
19
|
+
- !ruby/regexp /node_modules\//
|
20
|
+
- !ruby/regexp /tmp\//
|
21
|
+
- !ruby/regexp /vendor\//
|
22
|
+
- !ruby/regexp /hyper_resource/
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Aptible, Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#  Aptible::Resource
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/aptible-resource)
|
4
|
+
[](https://travis-ci.org/aptible/aptible-resource)
|
5
|
+
[](https://gemnasium.com/aptible/aptible-resource)
|
6
|
+
|
7
|
+
Foundation classes for Aptible resource server gems.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
To build a new resource server gem on top of `aptible-resource`, create a top-level class for your resource server. For example:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
module Example
|
15
|
+
module Api
|
16
|
+
class Resource < Aptible::Resource::Base
|
17
|
+
def namespace
|
18
|
+
'Example::Api'
|
19
|
+
end
|
20
|
+
|
21
|
+
def root_url
|
22
|
+
'https://api.example.com'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Then add the gem to your gemspec:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
spec.add_dependency 'aptible-resource'
|
33
|
+
```
|
34
|
+
|
35
|
+
## Development
|
36
|
+
|
37
|
+
This gem depends on a vendored version of [HyperResource](https://github.com/gamache/hyperresource), which can be updated from a local checkout of HyperResource as follows:
|
38
|
+
|
39
|
+
cp -rp /path/to/hyperresource/lib/hyper_resource* lib/
|
40
|
+
|
41
|
+
## Contributing
|
42
|
+
|
43
|
+
1. Fork the project.
|
44
|
+
1. Commit your changes, with specs.
|
45
|
+
1. Ensure that your code passes specs (`rake spec`) and meets Aptible's Ruby style guide (`rake rubocop`).
|
46
|
+
1. Create a new pull request on GitHub.
|
47
|
+
|
48
|
+
## Copyright and License
|
49
|
+
|
50
|
+
MIT License, see [LICENSE](LICENSE.md) for details.
|
51
|
+
|
52
|
+
Copyright (c) 2014 [Aptible](https://www.aptible.com), Frank Macreery, and contributors.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'English'
|
6
|
+
require 'aptible/resource/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'aptible-resource'
|
10
|
+
spec.version = Aptible::Resource::VERSION
|
11
|
+
spec.authors = ['Frank Macreery']
|
12
|
+
spec.email = ['frank@macreery.com']
|
13
|
+
spec.description = %q(Foundation classes for Aptible resource server gems)
|
14
|
+
spec.summary = %q(Foundation classes for Aptible resource server gems)
|
15
|
+
spec.homepage = 'https://github.com/aptible/aptible-resource'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($RS)
|
19
|
+
spec.test_files = spec.files.grep(/^spec\//)
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
# HyperResource dependencies
|
23
|
+
spec.add_dependency 'uri_template', '>= 0.5.2'
|
24
|
+
spec.add_dependency 'faraday', '>= 0.8.6'
|
25
|
+
spec.add_dependency 'json'
|
26
|
+
|
27
|
+
spec.add_dependency 'fridge'
|
28
|
+
spec.add_dependency 'activesupport'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
31
|
+
spec.add_development_dependency 'aptible-tasks'
|
32
|
+
spec.add_development_dependency 'rake'
|
33
|
+
spec.add_development_dependency 'rspec', '~> 2.0'
|
34
|
+
spec.add_development_dependency 'pry'
|
35
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Aptible
|
2
|
+
module Resource
|
3
|
+
class Adapter < HyperResource::Adapter::HAL_JSON
|
4
|
+
class << self
|
5
|
+
def get_data_type_from_object(object)
|
6
|
+
return nil unless object
|
7
|
+
|
8
|
+
if (type = object['_type'])
|
9
|
+
if type.respond_to?(:camelize)
|
10
|
+
type.camelize
|
11
|
+
else
|
12
|
+
type[0].upcase + type[1..-1]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'fridge'
|
2
|
+
|
3
|
+
# Require vendored HyperResource
|
4
|
+
$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
|
5
|
+
require 'hyper_resource'
|
6
|
+
|
7
|
+
require 'aptible/resource/adapter'
|
8
|
+
require 'aptible/resource/model'
|
9
|
+
|
10
|
+
module Aptible
|
11
|
+
module Resource
|
12
|
+
class Base < HyperResource
|
13
|
+
include Model
|
14
|
+
|
15
|
+
attr_accessor :token
|
16
|
+
|
17
|
+
def self.get_data_type_from_response(response)
|
18
|
+
return nil unless response && response.body
|
19
|
+
adapter.get_data_type_from_object(adapter.deserialize(response.body))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.adapter
|
23
|
+
Aptible::Resource::Adapter
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
if options.is_a?(Hash)
|
28
|
+
self.token = options[:token]
|
29
|
+
|
30
|
+
options[:root] ||= root_url
|
31
|
+
options[:namespace] ||= namespace
|
32
|
+
options[:headers] ||= { 'Content-Type' => 'application/json' }
|
33
|
+
options[:headers].merge!(
|
34
|
+
'Authorization' => "Bearer #{bearer_token}"
|
35
|
+
) if options[:token]
|
36
|
+
end
|
37
|
+
|
38
|
+
super(options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def adapter
|
42
|
+
self.class.adapter
|
43
|
+
end
|
44
|
+
|
45
|
+
def namespace
|
46
|
+
fail 'Resource server namespace must be defined by subclass'
|
47
|
+
end
|
48
|
+
|
49
|
+
def root_url
|
50
|
+
fail 'Resource server root URL must be defined by subclass'
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_by_url(url_or_href)
|
54
|
+
resource = dup
|
55
|
+
resource.href = url_or_href.gsub(/^#{root}/, '')
|
56
|
+
resource.get
|
57
|
+
end
|
58
|
+
|
59
|
+
def bearer_token
|
60
|
+
case token
|
61
|
+
when Aptible::Resource::Base then token.access_token
|
62
|
+
when Fridge::AccessToken then token.to_s
|
63
|
+
when String then token
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
require 'aptible/resource/token'
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module Aptible
|
4
|
+
module Resource
|
5
|
+
module Model
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def collection_href
|
12
|
+
"/#{basename}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def basename
|
16
|
+
name.split('::').last.downcase.pluralize
|
17
|
+
end
|
18
|
+
|
19
|
+
def all(options = {})
|
20
|
+
resource = find_by_url(collection_href, options)
|
21
|
+
return [] unless resource
|
22
|
+
resource.send(basename).entries
|
23
|
+
end
|
24
|
+
|
25
|
+
def find(id, options = {})
|
26
|
+
find_by_url("#{collection_href}/#{id}", options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_by_url(url, options = {})
|
30
|
+
# REVIEW: Should exception be raised if return type mismatch?
|
31
|
+
new(options).find_by_url(url)
|
32
|
+
rescue HyperResource::ClientError => e
|
33
|
+
if e.response.status == 404
|
34
|
+
return nil
|
35
|
+
else
|
36
|
+
raise e
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def create(params)
|
41
|
+
token = params.delete(:token)
|
42
|
+
resource = new(token: token)
|
43
|
+
resource.send(basename).create(normalize_params(params))
|
44
|
+
end
|
45
|
+
|
46
|
+
# rubocop:disable PredicateName
|
47
|
+
def has_many(relation)
|
48
|
+
define_has_many_getter(relation)
|
49
|
+
define_has_many_setter(relation)
|
50
|
+
end
|
51
|
+
# rubocop:enable PredicateName
|
52
|
+
|
53
|
+
def belongs_to(relation)
|
54
|
+
define_method relation do
|
55
|
+
get unless loaded
|
56
|
+
if (memoized = instance_variable_get("@#{relation}"))
|
57
|
+
memoized
|
58
|
+
elsif links[relation]
|
59
|
+
instance_variable_set("@#{relation}", links[relation].get)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# rubocop:disable PredicateName
|
65
|
+
def has_one(relation)
|
66
|
+
# Better than class << self + alias_method?
|
67
|
+
belongs_to(relation)
|
68
|
+
end
|
69
|
+
# rubocop:enable PredicateName
|
70
|
+
|
71
|
+
def define_has_many_getter(relation)
|
72
|
+
define_method relation do
|
73
|
+
get unless loaded
|
74
|
+
if (memoized = instance_variable_get("@#{relation}"))
|
75
|
+
memoized
|
76
|
+
elsif links[relation]
|
77
|
+
instance_variable_set("@#{relation}", links[relation].entries)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def define_has_many_setter(relation)
|
83
|
+
define_method "create_#{relation.to_s.singularize}" do |params = {}|
|
84
|
+
get unless loaded
|
85
|
+
links[relation].create(self.class.normalize_params(params))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def normalize_params(params = {})
|
90
|
+
params_array = params.map do |key, value|
|
91
|
+
value.is_a?(HyperResource) ? [key, value.href] : [key, value]
|
92
|
+
end
|
93
|
+
Hash[params_array]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete
|
98
|
+
# HyperResource/Faraday choke on empty response bodies
|
99
|
+
super
|
100
|
+
rescue HyperResource::ResponseError
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
alias_method :destroy, :delete
|
104
|
+
|
105
|
+
def update(params)
|
106
|
+
super(self.class.normalize_params(params))
|
107
|
+
end
|
108
|
+
|
109
|
+
# NOTE: The following does not update the object in-place
|
110
|
+
def reload
|
111
|
+
self.class.find_by_url(href, headers: headers)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'hyper_resource/attributes'
|
2
|
+
require 'hyper_resource/exceptions'
|
3
|
+
require 'hyper_resource/link'
|
4
|
+
require 'hyper_resource/links'
|
5
|
+
require 'hyper_resource/objects'
|
6
|
+
require 'hyper_resource/response'
|
7
|
+
require 'hyper_resource/version'
|
8
|
+
|
9
|
+
require 'hyper_resource/adapter'
|
10
|
+
require 'hyper_resource/adapter/hal_json'
|
11
|
+
|
12
|
+
require 'hyper_resource/modules/http'
|
13
|
+
require 'hyper_resource/modules/internal_attributes'
|
14
|
+
|
15
|
+
require 'rubygems' if RUBY_VERSION[0..2] == '1.8'
|
16
|
+
|
17
|
+
require 'pp'
|
18
|
+
|
19
|
+
## HyperResource is the main resource base class. Normally it will be used
|
20
|
+
## through subclassing, though it may also be used directly.
|
21
|
+
|
22
|
+
class HyperResource
|
23
|
+
|
24
|
+
include HyperResource::Modules::HTTP
|
25
|
+
include HyperResource::Modules::InternalAttributes
|
26
|
+
include Enumerable
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
DEFAULT_HEADERS = { 'Accept' => 'application/json' }
|
31
|
+
|
32
|
+
public
|
33
|
+
|
34
|
+
## Create a new HyperResource, given a hash of options. These options
|
35
|
+
## include:
|
36
|
+
##
|
37
|
+
## [root] The root URL of the resource.
|
38
|
+
##
|
39
|
+
## [auth] Authentication information. Currently only +{basic:
|
40
|
+
## ['key', 'secret']}+ is supported.
|
41
|
+
##
|
42
|
+
## [namespace] Class or class name, into which resources should be
|
43
|
+
## instantiated.
|
44
|
+
##
|
45
|
+
## [headers] Headers to send along with requests for this resource (as
|
46
|
+
## well as its eventual child resources, if any).
|
47
|
+
##
|
48
|
+
## [faraday_options] Configuration passed to +Faraday::Connection.initialize+,
|
49
|
+
## such as +{request: {timeout: 30}}+.
|
50
|
+
##
|
51
|
+
def initialize(opts={})
|
52
|
+
return init_from_resource(opts) if opts.kind_of?(HyperResource)
|
53
|
+
|
54
|
+
self.root = opts[:root] || self.class.root
|
55
|
+
self.href = opts[:href] || ''
|
56
|
+
self.auth = (self.class.auth || {}).merge(opts[:auth] || {})
|
57
|
+
self.namespace = opts[:namespace] || self.class.namespace
|
58
|
+
self.headers = DEFAULT_HEADERS.merge(self.class.headers || {}).
|
59
|
+
merge(opts[:headers] || {})
|
60
|
+
self.faraday_options = opts[:faraday_options] ||
|
61
|
+
self.class.faraday_options || {}
|
62
|
+
|
63
|
+
## There's a little acrobatics in getting Attributes, Links, and Objects
|
64
|
+
## into the correct subclass.
|
65
|
+
if self.class != HyperResource
|
66
|
+
if self.class::Attributes == HyperResource::Attributes
|
67
|
+
Object.module_eval(
|
68
|
+
"class #{self.class}::Attributes < HyperResource::Attributes; end"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
if self.class::Links == HyperResource::Links
|
72
|
+
Object.module_eval(
|
73
|
+
"class #{self.class}::Links < HyperResource::Links; end"
|
74
|
+
)
|
75
|
+
end
|
76
|
+
if self.class::Objects == HyperResource::Objects
|
77
|
+
Object.module_eval(
|
78
|
+
"class #{self.class}::Objects < HyperResource::Objects; end"
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
self.attributes = self.class::Attributes.new(self)
|
84
|
+
self.links = self.class::Links.new(self)
|
85
|
+
self.objects = self.class::Objects.new(self)
|
86
|
+
|
87
|
+
self.loaded = false
|
88
|
+
|
89
|
+
self.adapter = opts[:adapter] || self.class.adapter ||
|
90
|
+
HyperResource::Adapter::HAL_JSON
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
## Returns true if one or more of this object's attributes has been
|
95
|
+
## reassigned.
|
96
|
+
def changed?(*args)
|
97
|
+
attributes.changed?(*args)
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
#### Filters
|
102
|
+
|
103
|
+
## +incoming_body_filter+ filters a hash of attribute keys and values
|
104
|
+
## on their way from a response body to a HyperResource. Override this
|
105
|
+
## in a subclass of HyperResource to implement filters on incoming data.
|
106
|
+
def incoming_body_filter(attr_hash)
|
107
|
+
attr_hash
|
108
|
+
end
|
109
|
+
|
110
|
+
## +outgoing_body_filter+ filters a hash of attribute keys and values
|
111
|
+
## on their way from a HyperResource to a request body. Override this
|
112
|
+
## in a subclass of HyperResource to implement filters on outgoing data.
|
113
|
+
def outgoing_body_filter(attr_hash)
|
114
|
+
attr_hash
|
115
|
+
end
|
116
|
+
|
117
|
+
## +outgoing_uri_filter+ filters a hash of attribute keys and values
|
118
|
+
## on their way from a HyperResource to a URL. Override this
|
119
|
+
## in a subclass of HyperResource to implement filters on outgoing URI
|
120
|
+
## parameters.
|
121
|
+
def outgoing_uri_filter(attr_hash)
|
122
|
+
attr_hash
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
#### Enumerable support
|
127
|
+
|
128
|
+
## Returns the *i*th object in the first collection of objects embedded
|
129
|
+
## in this resource. Returns nil on failure.
|
130
|
+
def [](i)
|
131
|
+
get unless loaded
|
132
|
+
self.objects.first[1][i] rescue nil
|
133
|
+
end
|
134
|
+
|
135
|
+
## Iterates over the objects in the first collection of embedded objects
|
136
|
+
## in this resource.
|
137
|
+
def each(&block)
|
138
|
+
get unless loaded
|
139
|
+
self.objects.first[1].each(&block) rescue nil
|
140
|
+
end
|
141
|
+
|
142
|
+
#### Magic
|
143
|
+
|
144
|
+
## method_missing will load this resource if not yet loaded, then
|
145
|
+
## attempt to delegate to +attributes+, then +objects+, then +links+.
|
146
|
+
## Override with care.
|
147
|
+
def method_missing(method, *args)
|
148
|
+
self.get unless self.loaded
|
149
|
+
|
150
|
+
method = method.to_s
|
151
|
+
if method[-1,1] == '='
|
152
|
+
return attributes[method[0..-2]] = args.first if attributes[method[0..-2]]
|
153
|
+
else
|
154
|
+
return attributes[method] if attributes && attributes.has_key?(method)
|
155
|
+
return objects[method] if objects && objects[method]
|
156
|
+
if links && links[method]
|
157
|
+
if args.count > 0
|
158
|
+
return links[method].where(*args)
|
159
|
+
else
|
160
|
+
return links[method]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
raise NoMethodError, "undefined method `#{method}' for #{self.inspect}"
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
def inspect # @private
|
170
|
+
"#<#{self.class}:0x#{"%x" % self.object_id} @root=#{self.root.inspect} "+
|
171
|
+
"@href=#{self.href.inspect} @loaded=#{self.loaded} "+
|
172
|
+
"@namespace=#{self.namespace.inspect} ...>"
|
173
|
+
end
|
174
|
+
|
175
|
+
## +response_body+, +response_object+, and +deserialized_response+
|
176
|
+
## are deprecated in favor of +body+. (Sorry. Naming things is hard.)
|
177
|
+
def response_body # @private
|
178
|
+
_hr_deprecate('HyperResource#response_body is deprecated. '+
|
179
|
+
'Please use HyperResource#body instead.')
|
180
|
+
body
|
181
|
+
end
|
182
|
+
def response_object # @private
|
183
|
+
_hr_deprecate('HyperResource#response_object is deprecated. '+
|
184
|
+
'Please use HyperResource#body instead.')
|
185
|
+
body
|
186
|
+
end
|
187
|
+
def deserialized_response # @private
|
188
|
+
_hr_deprecate('HyperResource#deserialized_response is deprecated. '+
|
189
|
+
'Please use HyperResource#body instead.')
|
190
|
+
body
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
|
195
|
+
## Return a new HyperResource based on this object and a given href.
|
196
|
+
def _hr_new_from_link(href) # @private
|
197
|
+
self.class.new(:root => self.root,
|
198
|
+
:auth => self.auth,
|
199
|
+
:headers => self.headers,
|
200
|
+
:namespace => self.namespace,
|
201
|
+
:faraday_options => self.faraday_options,
|
202
|
+
:href => href)
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
## Returns the class into which the given response should be cast.
|
207
|
+
## If the object is not loaded yet, or if +namespace+ is
|
208
|
+
## not set, returns +self+.
|
209
|
+
##
|
210
|
+
## Otherwise, +response_class+ uses +get_data_type_from_response+ to
|
211
|
+
## determine subclass name, glues it to the given namespace, and
|
212
|
+
## creates the class if it's not there yet. E.g., given a namespace of
|
213
|
+
## +FooAPI+ and a response content-type of
|
214
|
+
## "application/vnd.foocorp.fooapi.v1+json;type=User", this should
|
215
|
+
## return +FooAPI::User+ (even if +FooAPI::User+ hadn't existed yet).
|
216
|
+
def self.response_class(response, namespace)
|
217
|
+
if self.to_s == 'HyperResource'
|
218
|
+
return self unless namespace
|
219
|
+
end
|
220
|
+
|
221
|
+
namespace ||= self.to_s
|
222
|
+
|
223
|
+
type_name = self.get_data_type_from_response(response)
|
224
|
+
return self unless type_name
|
225
|
+
|
226
|
+
namespaced_class(type_name, namespace)
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.namespaced_class(type_name, namespace)
|
230
|
+
class_name = "#{namespace}::#{type_name}"
|
231
|
+
class_name.gsub!(/[^_0-9A-Za-z:]/, '') ## sanitize class_name
|
232
|
+
|
233
|
+
## Return data type class if it exists
|
234
|
+
klass = eval(class_name) rescue :sorry_dude
|
235
|
+
return klass if klass.is_a?(Class)
|
236
|
+
|
237
|
+
## Data type class didn't exist -- create namespace (if necessary),
|
238
|
+
## then the data type class
|
239
|
+
if namespace != ''
|
240
|
+
nsc = eval(namespace) rescue :bzzzzzt
|
241
|
+
unless nsc.is_a?(Class)
|
242
|
+
Object.module_eval "class #{namespace} < #{self}; end"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
Object.module_eval "class #{class_name} < #{namespace}; end"
|
246
|
+
eval(class_name)
|
247
|
+
end
|
248
|
+
|
249
|
+
def _hr_response_class # @private
|
250
|
+
self.namespace ||= self.class.to_s unless self.class.to_s=='HyperResource'
|
251
|
+
self.class.response_class(self.response, self.namespace)
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
## Inspects the given Faraday::Response, and returns a string describing
|
256
|
+
## this resource's data type.
|
257
|
+
##
|
258
|
+
## By default, this method looks for a +type=...+ modifier in the
|
259
|
+
## response's +Content-type+ and returns that value, capitalized.
|
260
|
+
##
|
261
|
+
## Override this method in a subclass to alter HyperResource's behavior.
|
262
|
+
def self.get_data_type_from_response(response)
|
263
|
+
return nil unless response
|
264
|
+
return nil unless content_type = response['content-type']
|
265
|
+
return nil unless m=content_type.match(/;\s* type=([0-9A-Za-z:]+)/x)
|
266
|
+
m[1][0,1].upcase + m[1][1..-1]
|
267
|
+
end
|
268
|
+
|
269
|
+
## Uses +HyperResource.get_response_data_type+ to determine the proper
|
270
|
+
## data type for this object. Override to change behavior (though you
|
271
|
+
## probably just want to override the class method).
|
272
|
+
def get_data_type_from_response
|
273
|
+
self.class.get_data_type_from_response(self.response)
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
## Return this object, "cast" into its proper response class.
|
279
|
+
def to_response_class
|
280
|
+
response_class = self._hr_response_class
|
281
|
+
return self if self.class == response_class
|
282
|
+
response_class.new(self)
|
283
|
+
end
|
284
|
+
|
285
|
+
## Use the given resource's data to initialize this one.
|
286
|
+
def init_from_resource(resource)
|
287
|
+
(self.class._hr_attributes - [:attributes, :links, :objects]).each do |attr|
|
288
|
+
self.send("#{attr}=".to_sym, resource.send(attr))
|
289
|
+
end
|
290
|
+
self.adapter.apply(self.body, self)
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
## Show a deprecation message.
|
295
|
+
def self._hr_deprecate(message) # @private
|
296
|
+
STDERR.puts "#{message} (called from #{caller[2]})"
|
297
|
+
end
|
298
|
+
|
299
|
+
def _hr_deprecate(*args) # @private
|
300
|
+
self.class._hr_deprecate(*args)
|
301
|
+
end
|
302
|
+
end
|