module Eco module API module Organization class PresetsFactory class << self def all_abilities(hash = {}) Hash[abilities.each_with_object(nil).to_a].merge(hash) end def abilities_model @abilities_model ||= JSON.load(File.open(ABILITIES)) end def integrity_model @integrity_model ||= JSON.load(File.open(INTEGRITY)) end def abilities @abilities ||= abilities_model.keys end end ABILITIES = File.join(__dir__, 'presets_values.json') INTEGRITY = File.join(__dir__, 'presets_integrity.json') DEFAULT_CUSTOM = 'presets_custom.json' DEFAULT_MAP = 'presets_map.json' def initialize(presets_custom: DEFAULT_CUSTOM, presets_map: DEFAULT_MAP, enviro: nil, policy_groups: nil) fatal("Expecting Environment object. Given: #{enviro}") if enviro && !enviro.is_a?(Eco::API::Common::Session::Environment) @enviro = enviro @policy_groups = policy_groups @presets_custom_file = presets_custom || DEFAULT_CUSTOM @presets_map_file = presets_map || DEFAULT_MAP end def new(*policy_group_ids_or_names) names = policy_group_ids_or_names.map do |id_name| policy_groups.to_name(id_name)&.downcase end.compact if presets_map preset_names = names.map { |name| presets_map.fetch(name, nil) } else # option to do not use preset mapping (so just the policy group name) preset_names = names end compile(*preset_names) end # @return [Array] all the abilities def keys abilities_model.keys end private def compile(*preset_names) fatal("You need to specify an existing file for the custom presets.") if !@presets_custom @presets_custom.values_at(*preset_names).compact.reduce({}) do |p1, p2| merge(p1, p2) end end def merge(preset1, preset2) keys = preset1.keys | preset2.keys abilities_model.each_with_object({}) do |(key, values), result| next unless keys.include?(key) idx = [ values.index(preset1[key]), values.index(preset2[key]) ].compact.max result[key] = idx && values[idx] end end # unsused: only play with the given abilities def empty_model JSON.parse(abilities_model.to_json).transform_values {|v| nil } end def preset_errors(preset) return "No preset given" if !preset errors = preset.map do |k, v| value_exists?(k, v) ? nil : "#{k}:#{v}" end.compact return " Unknown: {#{errors.join(", ")}}" if errors.length > 0 nil end def preset_integrity(preset) preset.each_with_object([]) do |(ability, value), errors| next unless checks = integrity_model[ability] suberrors = [] checks.each do |check| next unless check["value"] == value check["conditions"].each do |cond, targets| case cond when "at_least" targets.each do |other, minimum| unless (ability_value_idx(other, minimum) <= ability_value_idx(other, preset[other])) suberrors << "'#{other}' should be at least '#{minimum}'" end end when "one_of" unless targets.any? {|other, expected| preset[other] == expected} suberrors << targets.each_with_object([]) do |(other, expected), out| out << "'#{other}': '#{expected}'" end.join(", ").yield_self do |msg| "there should be at least one of: {#{msg}}" end end else warn("Unsuported integrity condition statement '#{cond}' in '#{ability}' with level '#{value}'") end end end if suberrors.length > 0 errors << "Incorrect value '#{value}' for '#{ability}' - reasons: {#{suberrors.join(", ")}}" end end.yield_self do |errors| " Integrity errors: { #{errors.join(", ")} }" if errors.length > 0 end end def integrity_model self.class.integrity_model end def value_exists?(ability, value) abilities_model_inverted.dig(ability, value) end def abilities_model_inverted @abilities_model_inverted ||= abilities_model.each_with_object({}) do |(key, values), out| out[key] = values.each_with_object({}) {|v, h| h[v] = true } end end def ability_value_idx(ability, value) abilities_model[ability].index(value) end def abilities_model self.class.abilities_model end def policy_groups return @policy_groups if @policy_groups.is_a?(Eco::API::Organization::PolicyGroups) @policy_groups ||= @enviro&.api&.policy_groups.to_a unless @policy_groups.is_a?(Eco::API::Organization::PolicyGroups) @policy_groups = Eco::API::Organization::PolicyGroups.new(@policy_groups) end @policy_groups end def presets_custom return @presets_custom if instance_variable_defined?(:@presets_custom) @presets_custom = nil if @presets_custom_file if (file = File.expand_path(@presets_custom_file)) && File.exists?(file) @presets_custom = JSON.load(File.open(file)).tap do |custom_presets| errors = custom_presets.each_with_object([]) do |(key, preset), errors| if err = preset_errors(preset) errors << "{ '#{key}' preset -> #{err}}" end if err = preset_integrity(preset) errors << "{ '#{key}' preset -> #{err}}" end end fatal("File '#{file}' contains invalid presets:\n #{errors.join("\n ")}") if errors.length > 0 end end end end def presets_map return @presets_map if instance_variable_defined?(:@presets_map) @presets_map = nil if @presets_map_file if (file = File.expand_path(@presets_map_file)) && File.exists?(file) fatal("Maps file specified without 'presets_custom.json' file. Aborting!") if !presets_custom @presets_map = JSON.load(File.open(file)).tap do |map_presets| errors = [] if policy_groups.length > 0 errors = policy_groups.map do |pg| exists = map_presets[pg.name.downcase] || presets_custom[pg.name.downcase] exists ? nil : "'#{pg.name}'" end.compact warn("No maps or no preset for policy group(s): #{errors.join(", ")}") if errors.length > 0 end errors = map_presets.map do |source, dest| presets_custom[dest] ? nil : "'#{dest}'" end.compact warn("Unexisting mapped preset(s): #{errors.uniq.join(", ")}") if errors.length > 0 end end end end def fatal(msg) raise msg if !@enviro @enviro.logger.fatal(msg) raise msg end def warn(msg) raise msg if !@enviro @enviro.logger.warn(msg) end end end end end