module Restfully
# This class represents a Resource, which can be accessed and manipulated
# via HTTP methods.
#
# Some resources can be collection of other resources.
# In that case the Restfully::Collection module is included in the Restfully::Resource class.
# See the corresponding documentation for the list of additional methods that you can use on a collection resource.
class Resource
attr_reader :response, :request, :session
HIDDEN_PROPERTIES_REGEXP = /^\_\_(.+)\_\_$/
def initialize(session, response, request)
@session = session
@response = response
@request = request
@associations = {}
end
# Returns the value corresponding to the specified key,
# among the list of resource properties.
#
# e.g.:
# resource["uid"]
# => "rennes"
def [](key)
if !media_type.property(key) && !collection?
expand
end
media_type.property(key)
end
# Returns the resource URI.
def uri
request.uri
end
# Returns the Restfully::MediaType object that was used to parse the response.
def media_type
response.media_type
end
# Does this resource contain only a fragment of the full resource?
def complete?
media_type.complete?
end
# Is this resource a collection of items?
def collection?
media_type.collection?
end
# Returns the resource kind: "Collection" or "Resource".
def kind
collection? ? "Collection" : "Resource"
end
# Returns the "signature" of the resource. Used for (pretty-)inspection.
def signature(closed=true)
s = "#<#{kind}:0x#{object_id.to_s(16)}"
s += media_type.banner unless media_type.banner.nil?
# Only display path if host and port are the same than session's URI:
s += " uri=#{[uri.host, uri.port] == [
session.uri.host, session.uri.port
] ? uri.request_uri.inspect : uri.to_s.inspect}"
s += ">" if closed
s
end
# Load the resource. The options Hash can contain any number of parameters among which:
# :head:: a Hash of HTTP headers to pass when fetching the resource.
# :query:: a Hash of query parameters to add to the URI when fetching the resource.
def load(options = {})
# Send a GET request only if given a different set of options
if @request.update!(options) || @request.no_cache?
@response = @request.execute!
@request.remove_no_cache! if @request.forced_cache?
if session.process(@response, @request)
@associations.clear
else
raise Error, "Cannot reload the resource"
end
end
build
end
# Returns the list of relationships for this resource, extracted from the resource links ("rel" attribute).
def relationships
response.links.map(&:id).sort
end
# Returns the properties for this resource.
def properties
case props = media_type.property
when Hash
props.reject{|k,v|
# do not return keys used for internal use
k.to_s =~ HIDDEN_PROPERTIES_REGEXP
}
else
props
end
end
# Force reloading of the resource.
def reload
@request.no_cache!
load
end
# POST some payload on that resource URI.
# Either you pass a serialized payload as first argument, followed by an optional Hash of :head and :query parameters.
# Or you pass your payload as a Hash, and the serialization will occur based on the Content-Type you set (and only if a corresponding MediaType can be found in the MediaType catalog).
def submit(*args)
if allow?("POST")
@request.no_cache!
payload, options = extract_payload_from_args(args)
session.post(request.uri, payload, options)
else
raise MethodNotAllowed
end
end
# Send a DELETE HTTP request on the resource URI.
# See #load for the list of arguments this method can take.
def delete(options = {})
if allow?("DELETE")
@request.no_cache!
session.delete(request.uri)
else
raise MethodNotAllowed
end
end
# Send a PUT HTTP request with some payload on the resource URI.
# See #submit for the list of arguments this method can take.
def update(*args)
if allow?("PUT")
@request.no_cache!
payload, options = extract_payload_from_args(args)
session.put(request.uri, payload, options)
else
raise MethodNotAllowed
end
end
# Returns true if the resource supports the given HTTP method (String or Symbol).
def allow?(method)
response.allow?(method) || reload.response.allow?(method)
end
def inspect
if media_type.complete?
properties.inspect
else
"{...}"
end
end
def pretty_print(pp)
pp.text signature(false)
pp.nest 2 do
if relationships.length > 0
pp.breakable
pp.text "RELATIONSHIPS"
pp.nest 2 do
pp.breakable
pp.text "#{relationships.join(", ")}"
end
end
pp.breakable
if collection?
# display items
pp.text "ITEMS (#{offset}..#{offset+length})/#{total}"
pp.nest 2 do
self.each do |item|
pp.breakable
pp.text item.signature(true)
end
end
else
expand
pp.text "PROPERTIES"
pp.nest 2 do
properties.each do |key, value|
pp.breakable
pp.text "#{key.inspect}=>"
value.pretty_print(pp)
end
end
end
yield pp if block_given?
end
pp.text ">"
nil
end
# Build the resource after loading.
def build
metaclass = class << self; self; end
extend Collection if collection?
response.links.each do |link|
metaclass.send(:define_method, link.id.to_sym) do |*args|
session.get(link.href, :head => {
'Accept' => link.type
}).load(*args)
end
end
self
end
# Reload itself if the resource is not #complete?.
def expand
reload unless complete?
self
end
protected
def extract_payload_from_args(args)
options = args.extract_options!
head = options.delete(:headers) || options.delete(:head) || {}
head['Origin-Content-Type'] = response.head['Content-Type']
query = options.delete(:query)
payload = args.shift || options
options = {
:head => head, :query => query,
:serialization => media_type.property.reject{|k,v|
k !~ HIDDEN_PROPERTIES_REGEXP
}
}
[payload, options]
end
end
end