lib/redfish_client/resource.rb in redfish_client-0.4.1 vs lib/redfish_client/resource.rb in redfish_client-0.5.0
- old
+ new
@@ -15,55 +15,86 @@
# accessing `root.SessionService` will automatically fetch the appropriate
# resource from the API.
#
# In order to reduce the amount of requests being sent to the service,
# resource can also utilise caching connector. If we would like to get
- # fresh values from the service, {#reset} call will flush the cache,
- # causing next access to retrieve fresh data.
+ # fresh values from the service, {#refresh} call will flush the cache and
+ # retrieve fresh data from the remote.
class Resource
# NoODataId error is raised when operation would need OpenData id of the
# resource to accomplish the task a hand.
class NoODataId < StandardError; end
# NoResource error is raised if the service cannot find requested
# resource.
class NoResource < StandardError; end
+ # Timeout error is raised if the async request is not handled in due time.
+ class Timeout < StandardError; end
+
# Headers, returned from the service when resource has been constructed.
+ #
+ # @return [Hash] resource headers
attr_reader :headers
+ # Raw data that has been used to construct resource by either fetching it
+ # from the remote API or by being passed-in as a parameter to constructor.
+ #
+ # @return [Hash] resource raw data
+ attr_reader :raw
+
# Create new resource.
#
# Resource can be created either by passing in OpenData identifier or
# supplying the content (hash). In the first case, connector will be used
# to fetch the resource data. In the second case, resource only wraps the
# passed-in hash and does no fetching.
#
# @param connector [RedfishClient::Connector] connector that will be used
# to fetch the resources
# @param oid [String] OpenData id of the resource
- # @param content [Hash] content to populate resource with
+ # @param raw [Hash] raw content to populate resource with
# @raise [NoResource] resource cannot be retrieved from the service
- def initialize(connector, oid: nil, content: nil)
+ def initialize(connector, oid: nil, raw: nil)
@connector = connector
-
if oid
initialize_from_service(oid)
else
- @content = content
+ @raw = raw
end
end
+ # Wait for the potentially async operation to terminate
+ #
+ # Note that this can be safely called on response from non-async
+ # operations where the function will return immediately and without making
+ # any additional requests to the service.
+ #
+ # @param response [RedfishClient::Response] response
+ # @param retries [Integer] number of retries
+ # @param delay [Integer] number of seconds between retries
+ # @return [RedfishClient::Response] final response
+ # @raise [Timeout] if the operation did not terminate in time
+ def wait(response, retries: 10, delay: 1)
+ retries.times do |_i|
+ return response if response.done?
+
+ sleep(delay)
+ response = get(path: response.monitor)
+ end
+ raise Timeout, "Async operation did not terminate in allotted time"
+ end
+
# Access resource content.
#
# This function offers a way of accessing resource data in the same way
# that hash exposes its content.
#
# @param attr [String] key for accessing data
# @return associated value or `nil` if attr is missing
def [](attr)
- build_resource(@content[attr])
+ build_resource(raw[attr])
end
# Safely access nested resource content.
#
# This function is an equivalent of safe navigation operator that can be
@@ -80,11 +111,11 @@
# Test if resource contains required key.
#
# @param name [String, Symbol] key name to test
# @return [Boolean] inclusion test result
def key?(name)
- @content.key?(name.to_s)
+ raw.key?(name.to_s)
end
# Convenience access for resource data.
#
# Calling `resource.Value` is exactly the same as `resource["Value"]`.
@@ -94,29 +125,56 @@
def respond_to_missing?(symbol, include_private = false)
key?(symbol.to_s) || super
end
- # Clear the cached sub-resources.
+ # Pretty-print the wrapped content.
#
- # This method is a no-op if connector in use does not support caching.
- def reset
- @connector.reset if @connector.respond_to?(:reset)
+ # @return [String] JSON-serialized raw data
+ def to_s
+ JSON.pretty_generate(raw)
end
- # Access raw JSON data that resource wraps.
+ # Issue a requests to the selected endpoint.
#
- # @return [Hash] wrapped data
- def raw
- @content
+ # By default, request will be sent to the path, stored in `@odata.id`
+ # field. Source field can be changed by specifying the `field` parameter
+ # when calling this function. Specifying the `path` argument will bypass
+ # the field lookup altogether and issue a request directly to the selected
+ # path.
+ #
+ # If the resource has no lookup field, {NoODataId} error will be raised,
+ # since posting to non-networked resources makes no sense and probably
+ # indicates bug in library consumer.
+ #
+ # @param method [Symbol] HTTP method (:get, :post, :patch or :delete)
+ # @param field [String, Symbol] path lookup field
+ # @param path [String] path to post to
+ # @return [RedfishClient::Response] response
+ # @raise [NoODataId] resource has no OpenData id
+ def request(method, field, path, payload = nil)
+ @connector.request(method, get_path(field, path), payload)
end
- # Pretty-print the wrapped content.
+ # Issue a GET requests to the selected endpoint.
#
- # @return [String] JSON-serialized raw data
- def to_s
- JSON.pretty_generate(@content)
+ # By default, GET request will be sent to the path, stored in `@odata.id`
+ # field. Source field can be changed by specifying the `field` parameter
+ # when calling this function. Specifying the `path` argument will bypass
+ # the field lookup altogether and issue a GET request directly to the
+ # selected path.
+ #
+ # If the resource has no lookup field, {NoODataId} error will be raised,
+ # since posting to non-networked resources makes no sense and probably
+ # indicates bug in library consumer.
+ #
+ # @param field [String, Symbol] path lookup field
+ # @param path [String] path to post to
+ # @return [RedfishClient::Response] response
+ # @raise [NoODataId] resource has no OpenData id
+ def get(field: "@odata.id", path: nil)
+ request(:get, field, path)
end
# Issue a POST requests to the selected endpoint.
#
# By default, POST request will be sent to the path, stored in `@odata.id`
@@ -133,56 +191,82 @@
# indicates bug in library consumer.
#
# @param field [String, Symbol] path lookup field
# @param path [String] path to post to
# @param payload [Hash<String, >] data to send
- # @return [Excon::Response] response
+ # @return [RedfishClient::Response] response
# @raise [NoODataId] resource has no OpenData id
def post(field: "@odata.id", path: nil, payload: nil)
- @connector.post(get_path(field, path), payload)
+ request(:post, field, path, payload)
end
# Issue a PATCH requests to the selected endpoint.
#
# Works exactly the same as the {post} method, but issued a PATCH request
# to the server.
#
# @param field [String, Symbol] path lookup field
# @param path [String] path to patch
# @param payload [Hash<String, >] data to send
- # @return [Excon::Response] response
+ # @return [RedfishClient::Response] response
# @raise [NoODataId] resource has no OpenData id
def patch(field: "@odata.id", path: nil, payload: nil)
- @connector.patch(get_path(field, path), payload)
+ request(:patch, field, path, payload)
end
# Issue a DELETE requests to the endpoint of the resource.
#
# If the resource has no `@odata.id` field, {NoODataId} error will be
# raised, since deleting non-networked resources makes no sense and
# probably indicates bug in library consumer.
#
- # @return [Excon::Response] response
+ # @return [RedfishClient::Response] response
# @raise [NoODataId] resource has no OpenData id
- def delete
- @connector.delete(get_path("@odata.id", nil))
+ def delete(field: "@odata.id", path: nil, payload: nil)
+ request(:delete, field, path, payload)
end
+ # Refresh resource content from the API
+ #
+ # Caling this method will ensure that the resource data is in sync with
+ # the Redfis API, invalidating any caches as necessary.
+ def refresh
+ return unless self["@odata.id"]
+
+ # TODO(@tadeboro): raise more sensible exception if resource cannot be
+ # refreshed.
+ @connector.reset(self["@odata.id"])
+ initialize_from_service(self["@odata.id"])
+ end
+
private
def initialize_from_service(oid)
- resp = @connector.get(oid)
- raise NoResource unless resp.status == 200
+ url, fragment = oid.split("#", 2)
+ resp = wait(get(path: url))
+ raise NoResource unless [200, 201].include?(resp.status)
- @content = JSON.parse(resp.data[:body])
- @content["@odata.id"] = oid
- @headers = resp.data[:headers]
+ @raw = get_fragment(JSON.parse(resp.body), fragment)
+ @raw["@odata.id"] = oid
+ @headers = resp.headers
end
+ def get_fragment(data, fragment)
+ # data, /my/0/part -> data["my"][0]["part"]
+ parse_fragment_string(fragment).reduce(data) do |acc, c|
+ acc[acc.is_a?(Array) ? c.to_i : c]
+ end
+ end
+
+ def parse_fragment_string(fragment)
+ # /my/0/part -> ["my", "0", "part"]
+ fragment ? fragment.split("/").reject { |i| i == "" } : []
+ end
+
def get_path(field, path)
raise NoODataId if path.nil? && !key?(field)
- path || @content[field]
+ path || raw[field]
end
def build_resource(data)
return nil if data.nil?
@@ -195,10 +279,10 @@
def build_hash_resource(data)
if data.key?("@odata.id")
Resource.new(@connector, oid: data["@odata.id"])
else
- Resource.new(@connector, content: data)
+ Resource.new(@connector, raw: data)
end
rescue NoResource
nil
end
end