require 'json' require_relative 'helper' module Smash module CloudPowers # The Context class is the class that handles serializing the Context so that it # can be passed from node to node and rebuilt. This provides a way for nodes in # the same Brain to have an awareness of required communication modes, for example. # Because resources generally require some sort of information to connect with # or create and a Brain operates in the same Context, it can use the Context to # decouple the creation and usage from the coordination. class Context include Smash::CloudPowers::Helper attr_accessor :package # The given data structure that is used to build @structure attr_reader :structure # A Hash that is used to represent the scheme of the Context # Attempts to create a Context out of the argument(s) you've # passed. # === @params args # - exp Hash: # `{ task: 'demo', queue: [:backlog, :sned], pipe: :status_stream }` # each key is a module or class that is in CloudPowers and each value # is either an array of configurations information or a single configuration. # - exp Array # `[:task, 'demo', :queue, :backlog, :sned, :pipe, :status_stream]` # each key is followed by 0..n valid configurations information # === @returns # `Smash::Context` def initialize(args) unless valid_args?(args) raise ArgumentError.new 'Can be either a Hash, JSON, or an Enumerable ' + "arguments: #{args}" end @package = args @structure = decipher(args) end # # Decipher figures out which translation method to use by making some simple # # type checks, etc. and then routing the args to the correct method. def decipher(args) case args when Hash args when String translate_json(args) when Enumerable translate_list(args) end end # Creates a flat Array of the 2-D Array that gets returned from `#simplify()` # === @returns def flatten simplify.flatten end # Create an array version of @sructure by calling `#to_a()` on it # === @returns Array # TODO: Check if this has a limit to n-layers def simplify @structure.to_a end # A Hash that represents the resources and some configuration for them # === @returns Hash def structure modify_keys_with(@structure) { |key| key.to_sym } end # Valid scheme for @structure is assured by running the arguments through # the decipher method, which is how @structure is set in `#new(args)` def structure=(args) @structure = decipher(args) end # Parse the given JSON # === @param: json_string # === @returns def translate_json(json_string) begin JSON.parse(json_string) rescue JSON::ParserError raise ArgumentError "Incorrectly formatted JSON" end end # Re-layer this flattened Array or enumerable list so that it looks like the # Hash that it used to be before the Smash::Context#flatten() method was called # === @param list # `` # e.g. # flat # ```Ruby # [ # object_name_1, config_1a, config_2a, ..., # object_2, config_1b, etc, # ... # ] # ``` # grouped # or # ```Ruby # [ # [object_name_1, config_1a, config_2a, ...], # [object_2, config_1b, etc], # ... # ] # ``` # or # structured # ```Ruby # [ # [object_name_1, [config_1a, config_2a, ...]], # [object_2, [config_1b, etc]], # ... # ] # ``` # returns # ```Ruby # { # object_1: [config_1a, config_2a, ...], # object_2: [config_1b, ...], # ... # } # ``` # === @returns Hash # If `#valid_package_hash?()` is called on this Hash, it will return true def translate_list(list) list.first.kind_of?(Enumerable) ? translate_simplified(list) : translate_flattened(list) end # Translates an Array into a valid @structure Hash # === @param arr # e.g. # ```Ruby # [:task, 'demo', :queue, 'name1', {other config hash},...,:pipe, 'name1'] # ``` # === @returns Hash # calling `#valid_hash_format?()` on returned Hash will return true def translate_flattened(list) arr = list.to_a results = [] # get the indexes of CloudPowers resources in arr key_locations = arr.each_with_index.collect do |key, i, _| i if available_resources.include?(to_pascal(key).to_sym) end.reject(&:nil?) key_locations << arr.size # add the last index into the ranges # use each pair as ranges to slice up the Array key_locations.each_cons(2) do |current_i, next_i| results << [arr[current_i], arr[(current_i+1)..(next_i-1)]] end translate_simplified(results) end # Translates a 2D Array into a valid @structure Hash # === @param arr # e.g. # ``` # [ # [:task, 'demo'], # [:queue, 'name1', {other config hash},...], # [:pipe, 'name1'] # ... # ] # ``` # - Needs to be a 2D Array # - First object of each inner-Array is the key # - All following values in that inner Array are separate configuration # information pieces that can be used in the `#create!()` method for that particular resource # === @returns Hash # well formatted for the @structure def translate_simplified(arr) arr.inject({}) do |hash, key_config_map| hash.tap do |h| key = key_config_map.shift h[key.to_sym] = *key_config_map.flatten end end end # Uses `#to_json()` on the @structure # === @returns Hash def to_json structure.to_json end # The Context class can be initialized in any of the formats that a Context # class _should_ exist in. It can be a vanilla version, where the @structure # is a Hash, structured correctly or it can be serialized into JSON or it can # be an Array # === @params: args String # === @returns: Boolean def valid_args?(args) case args when Hash valid_hash_format?(args) when String valid_json_format?(args) when Enumerable valid_array_format?(args) else false end end # Makes sure that the list is enumerable and that at least the first term # is a resource name from Smash::CloudPowers. All other objects can # potentially be configurations. # @param list # @returns Boolean def valid_array_format?(list) use = list.first.kind_of?(Enumerable) ? list.first.first : list.first ((list.kind_of? Enumerable) && (available_resources.include?(to_pascal(use).to_sym))) end # Makes sure that each key is the name of something CloudPowers can interact with # @param hash # @returns Boolean def valid_hash_format?(hash) keys_arr = hash.keys.map { |key| to_pascal(key).to_sym } (keys_arr - available_resources).empty? end # Parses the json_string which yields a Hash or an exception. From there, # use the #valid_hash_format?() method def valid_json_format?(json_string) begin valid_hash_format?(JSON.parse(json_string)) rescue JSON::ParserError false end end end end end