# frozen_string_literal: true # Shared value validation and frozen editor-owned configuration objects. module Vamp MAX_FRAME = 4 * 1024 * 1024 # Validates a boundary value and returns a UTF-8 copy. # @param value [Object] # @param label [String] # @return [String] def self.string!(value, label) raise TypeError, "#{label} must be a String" unless value.is_a?(String) copy = value.dup.force_encoding(Encoding::UTF_8) raise ArgumentError, "#{label} must be valid UTF-8" unless copy.valid_encoding? copy end # Recursively validates JSON values and normalizes hash keys to strings. # @param value [Object] # @return [Hash{String => Object}, Array, String, Integer, Float, Boolean, nil] def self.json_value!(value) case value when Hash json_hash(value) when Array value.map { |item| json_value!(item) } else json_scalar(value) end end # Freezes the supplied value and every nested hash/array member in place. # @param value [Object] # @return [Object] the same value def self.deep_freeze(value) case value when Hash value.each do |key, item| deep_freeze(key) deep_freeze(item) end when Array value.each { |item| deep_freeze(item) } end value.freeze end # @!override Vamp::Action#name # @return [String] the catalog's dotted action name Action = Struct.new(:name) # @!override Vamp::Mode#name # @return [String] the lowercase binding mode Mode = Struct.new(:name) # Action methods and nested modules come exclusively from the Rust catalog. # @type [Module] Core = Module.new # @param names [Array] dotted core action names # @return [Array] frozen action namespaces def self.install_actions(names) raise TypeError, "core action catalog must be an Array" unless names.is_a?(Array) namespaces = [Core] names.each { |name| install_action(name, namespaces) } namespaces.each(&:freeze) end # Frozen binding mode values, distinct from a buffer's editing state. module Modes # @!method self.normal # @return [Vamp::Mode] # @!method self.insert # @return [Vamp::Mode] # @!method self.command # @return [Vamp::Mode] # @!method self.search # @return [Vamp::Mode] # @!method self.visual # @return [Vamp::Mode] # @!method self.selection # @return [Vamp::Mode] # @!method self.tree # @return [Vamp::Mode] %w[normal insert command search visual selection tree].each do |name| mode = Mode.new(name.freeze).freeze define_singleton_method(name) { mode } end end Action.private_class_method :new Mode.private_class_method :new # @param value [Hash{String, Symbol => Object}] # @return [Hash{String => Object}] def self.json_hash(value) value.to_h do |key, item| raise TypeError, "option keys must be Strings or Symbols" unless key.is_a?(String) || key.is_a?(Symbol) [string!(key.to_s, "option key"), json_value!(item)] end end # @param value [Object] # @return [String, Integer, Float, Boolean, nil] def self.json_scalar(value) case value when String string!(value, "option value") when Integer, TrueClass, FalseClass, NilClass, Float json_literal(value) else raise TypeError, "options must contain only JSON values" end end # @param value [Integer, Float, Boolean, nil] # @return [Integer, Float, Boolean, nil] def self.json_literal(value) raise ArgumentError, "option numbers must be finite" if value.is_a?(Float) && !value.finite? value end # @param name [String] # @param namespaces [Array] # @return [Symbol] the installed action method name def self.install_action(name, namespaces) name = action_name(name) parts = name.split(".").drop(1) namespace = parts[0...-1].reduce(Core) { |parent, part| action_namespace(parent, part, namespaces, name) } leaf = parts.last available_action!(namespace, leaf, name) action = Action.send(:new, name.freeze).freeze namespace.define_singleton_method(leaf) { action } end # @param name [Object] # @return [String] def self.action_name(name) name = string!(name, "core action name") raise ArgumentError, "invalid core action name: #{name}" unless name.match?(/\Acore(?:\.[a-z_][a-z0-9_]*)+\z/) name end # @param parent [Module] # @param part [String] # @param namespaces [Array] # @param name [String] # @return [Module] def self.action_namespace(parent, part, namespaces, name) return new_action_namespace(parent, part, namespaces) unless parent.singleton_methods(false).include?(part.to_sym) child = parent.public_send(part) raise ArgumentError, "conflicting core action namespace: #{name}" unless child.is_a?(Module) child end # @param parent [Module] # @param part [String] # @param namespaces [Array] # @return [Module] def self.new_action_namespace(parent, part, namespaces) child = Module.new parent.define_singleton_method(part) { child } namespaces << child child end # @param namespace [Module] # @param leaf [String] # @param name [String] # @return [nil] def self.available_action!(namespace, leaf, name) return unless namespace.singleton_methods(false).include?(leaf.to_sym) raise ArgumentError, "conflicting core action name: #{name}" end private_class_method :json_hash, :json_scalar, :json_literal, :install_action, :action_name, :action_namespace, :new_action_namespace, :available_action! end