lib/i18n-js/schema.rb in i18n-js-4.2.0 vs lib/i18n-js/schema.rb in i18n-js-4.2.1

- old
+ new

@@ -23,121 +23,194 @@ end def self.validate!(target) schema = new(target) schema.validate! - I18nJS.plugins.each {|plugin| plugin.validate_schema(config: target) } + schema end attr_reader :target def initialize(target) @target = target end def validate! - expect_type(:root, target, Hash, target) + validate_root - expect_required_keys(self.class.required_root_keys, target) - reject_extraneous_keys(self.class.root_keys, target) + expect_required_keys( + keys: self.class.required_root_keys, + path: nil + ) + + reject_extraneous_keys( + keys: self.class.root_keys, + path: nil + ) + validate_translations validate_lint_translations validate_lint_scripts + validate_plugins end + def validate_plugins + I18nJS.plugins.each do |plugin| + next unless target.key?(plugin.config_key) + + expect_type( + path: [plugin.config_key, :enabled], + types: [TrueClass, FalseClass] + ) + + plugin.validate_schema + end + end + + def validate_root + return if target.is_a?(Hash) + + message = "Expected config to be \"Hash\"; " \ + "got #{target.class} instead" + + reject message, target + end + def validate_lint_translations key = :lint_translations return unless target.key?(key) - config = target[key] + expect_type(path: [key], types: Hash) - expect_type(key, config, Hash, target) - expect_required_keys(REQUIRED_LINT_TRANSLATIONS_KEYS, config) - expect_type(:ignore, config[:ignore], Array, config) + expect_required_keys( + keys: REQUIRED_LINT_TRANSLATIONS_KEYS, + path: [key] + ) + + expect_type(path: [key, :ignore], types: Array) end def validate_lint_scripts key = :lint_scripts return unless target.key?(key) - config = target[key] - - expect_type(key, config, Hash, target) - expect_required_keys(REQUIRED_LINT_SCRIPTS_KEYS, config) - expect_type(:ignore, config[:ignore], Array, config) - expect_type(:patterns, config[:patterns], Array, config) + expect_type(path: [key], types: Hash) + expect_required_keys( + keys: REQUIRED_LINT_SCRIPTS_KEYS, + path: [key] + ) + expect_type(path: [key, :ignore], types: Array) + expect_type(path: [key, :patterns], types: Array) end def validate_translations - translations = target[:translations] + expect_array_with_items(path: [:translations]) - expect_type(:translations, translations, Array, target) - expect_array_with_items(:translations, translations) - - translations.each do |translation| - validate_translation(translation) + target[:translations].each_with_index do |translation, index| + validate_translation(translation, index) end end - def validate_translation(translation) - expect_required_keys(REQUIRED_TRANSLATION_KEYS, translation) - reject_extraneous_keys(TRANSLATION_KEYS, translation) - expect_type(:file, translation[:file], String, translation) - expect_type(:patterns, translation[:patterns], Array, translation) - expect_array_with_items(:patterns, translation[:patterns], translation) + def validate_translation(_translation, index) + expect_required_keys( + path: [:translations, index], + keys: REQUIRED_TRANSLATION_KEYS + ) + + reject_extraneous_keys( + keys: TRANSLATION_KEYS, + path: [:translations, index] + ) + + expect_type(path: [:translations, index, :file], types: String) + expect_array_with_items(path: [:translations, index, :patterns]) end def reject(error_message, node = nil) node_json = "\n#{JSON.pretty_generate(node)}" if node raise InvalidError, "#{error_message}#{node_json}" end - def expect_enabled_config(config_key, value) - return if [TrueClass, FalseClass].include?(value.class) + def expect_type(path:, types:) + path = prepare_path(path: path) + value = value_for(path: path) + types = Array(types) - actual_type = value.class + return if types.any? {|type| value.is_a?(type) } - reject "Expected #{config_key}.enabled to be a boolean; " \ - "got #{actual_type} instead" - end - - def expect_type(attribute, value, expected_type, payload) - return if value.is_a?(expected_type) - actual_type = value.class + type_desc = if types.size == 1 + types[0].to_s.inspect + else + "one of #{types.inspect}" + end + message = [ - "Expected #{attribute.inspect} to be #{expected_type};", + "Expected #{path.join('.').inspect} to be #{type_desc};", "got #{actual_type} instead" ].join(" ") - reject message, payload + reject message, target end - def expect_array_with_items(attribute, value, payload = value) + def expect_array_with_items(path:) + expect_type(path: path, types: Array) + + path = prepare_path(path: path) + value = value_for(path: path) + return unless value.empty? - reject "Expected #{attribute.inspect} to have at least one item", payload + reject "Expected #{path.join('.').inspect} to have at least one item", + target end - def expect_required_keys(required_keys, value) - keys = value.keys.map(&:to_sym) + def expect_required_keys(keys:, path:) + path = prepare_path(path: path) + value = value_for(path: path) + actual_keys = value.keys.map(&:to_sym) - required_keys.each do |key| - next if keys.include?(key) + keys.each do |key| + next if actual_keys.include?(key) - reject "Expected #{key.inspect} to be defined", value + path_desc = if path.empty? + key.to_s.inspect + else + (path + [key]).join(".").inspect + end + + reject "Expected #{path_desc} to be defined", target end end - def reject_extraneous_keys(allowed_keys, value) - keys = value.keys.map(&:to_sym) - extraneous = keys.to_a - allowed_keys.to_a + def reject_extraneous_keys(keys:, path:) + path = prepare_path(path: path) + value = value_for(path: path) + actual_keys = value.keys.map(&:to_sym) + extraneous = actual_keys.to_a - keys.to_a + return if extraneous.empty? - reject "Unexpected keys: #{extraneous.join(', ')}", value + path_desc = if path.empty? + "config" + else + path.join(".").inspect + end + + reject "#{path_desc} has unexpected keys: #{extraneous.inspect}", + target + end + + def prepare_path(path:) + path = path.to_s.split(".").map(&:to_sym) unless path.is_a?(Array) + path + end + + def value_for(path:) + path.empty? ? target : target.dig(*path) end end end