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. # # Parameters # * args +Hash+|+JSON+|+Array+|+enumerable list+ # * * expample +Hash+: # * * * 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. # { task: 'demo', queue: [:backlog, :sned], pipe: :status_stream } # * * expample Array # * * * each key is followed by 0..n valid configurations information # [:task, 'demo', :queue, :backlog, :sned, :pipe, :status_stream] # # 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. # # Notes # * See +#translate_json()+ # * See +#translate_list()+ 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 +Array+ # # Example # example_context.structure # # => { task: 'demo', queue: [:backlog, :sned], pipe: [:status_stream] } # example.flatten # # => [:task, 'demo', :queue, :backlog, :sned, :pipe, :status_stream] # # Notes # * See +#simplify() # * See +#Array::flatten()+ def flatten simplify.flatten end # Create an array version of @sructure by calling `#to_a()` on it # # Returns # Array[Array..n] # # Example # example_context.structure # # => { task: 'demo', queue: [:backlog, :sned], pipe: [:status_stream] } # example.simplify # # => [:task, 'demo', :queue, [:backlog, :sned], :pipe, [:status_stream]] # # Notes # * This uses the different Constants, like Smash, Synapse and anything it can find # to decide what should be used as a key and what its following values array should # contain. It basically says: # 1. if the nth item is a known key (from the above search), add it as an object in the Array. # 2. else, add it to the last sub-Array # 3. move to n+1 in the +structure Hash+ # * 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 # Parameters: 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 # Parameters 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 # Parameters 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 # Parameters 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. # Parameters 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 # Parameters 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