module PandaPal module OrganizationConcerns module SettingsValidation extend ActiveSupport::Concern included do validate :validate_settings end class_methods do def settings_structure if PandaPal.lti_options&.[](:settings_structure).present? normalize_settings_structure(PandaPal.lti_options[:settings_structure]) else { type: Hash, allow_additional: true, properties: {}, } end end def normalize_settings_structure(struc) return {} unless struc.present? return struc if struc[:properties] || struc[:type] || struc.key?(:required) struc = struc.dup nstruc = {} nstruc[:type] = struc.delete(:data_type) if struc.key?(:data_type) nstruc[:required] = struc.delete(:is_required) if struc.key?(:is_required) nstruc[:properties] = struc.map { |k, sub| [k, normalize_settings_structure(sub)] }.to_h if struc.present? nstruc end end def settings_structure self.class.settings_structure end def validate_settings validate_settings_level(settings || {}, settings_structure).each do |err| errors[:settings] << err end end private def validate_settings_level(settings, spec, path: [], errors: []) human_path = "[:#{path.join('][:')}]" if settings.nil? errors << "Entry #{human_path} is required" if spec[:required] return errors end if spec[:type] norm_types = Array(spec[:type]).map do |t| if [:bool, :boolean, 'Bool', 'Boolean'].include?(t) 'Boolean' elsif t.is_a?(String) t.constantize else t end end any_match = norm_types.any? do |t| if t == 'Boolean' settings == true || settings == false else settings.is_a?(t) end end unless any_match errors << "Expected #{human_path} to be a #{spec[:type]}. Was a #{settings.class.to_s}" return errors end end if spec[:validate].present? val_errors = [] if spec[:validate].is_a?(Symbol) proc_result = send(spec[:validate], settings, spec, path: path, errors: val_errors) elsif spec[:validate].is_a?(String) split_val = spec[:validate].split?('.') split_val << 'validate_settings' if split_val.count == 1 resolved_module = split_val[0].constantize proc_result = resolved_module.send(split_val[1].to_sym, settings, spec, path: path, errors: val_errors) elsif spec[:validate].is_a?(Proc) proc_result = instance_exec(settings, spec, path: path, errors: val_errors, &spec[:validate]) end val_errors << proc_result unless val_errors.present? || proc_result == val_errors val_errors = val_errors.flatten.uniq.compact.map do |ve| ve.gsub('', human_path) end errors.concat(val_errors) end if settings.is_a?(Array) if spec[:length].is_a?(Range) errors << "#{human_path} should contain #{spec[:length]} items" unless spec[:length].include?(settings.count) elsif spec[:length].is_a?(Numeric) errors << "#{human_path} should contain exactly #{spec[:length]} items" unless spec[:length] == settings.count end if spec[:item] != nil settings.each_with_index do |value, i| validate_settings_level(settings[i], spec[:item], path: [*path, i], errors: errors) end end end if settings.is_a?(Hash) if spec[:properties] != nil spec[:properties].each do |key, pspec| validate_settings_level(settings[key], pspec, path: [*path, key], errors: errors) end end if spec[:properties] != nil || spec[:allow_additional] != nil extra_keys = settings.keys - (spec[:properties]&.keys || []) if extra_keys.present? if spec[:allow_additional].is_a?(Hash) extra_keys.each do |key| validate_settings_level(settings[key], spec[:allow_additional], path: [*path, key], errors: errors) end elsif !spec[:allow_additional] errors << "Did not expect #{human_path} to contain [#{extra_keys.join(', ')}]" end end end end errors end end end end