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