rkh/mustermann

View on GitHub
mustermann/lib/mustermann/concat.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true
module Mustermann
  # Class for pattern objects that are a concatenation of other patterns.
  # @see Mustermann::Pattern#+
  class Concat < Composite
    # Mixin for patterns to support native concatenation.
    # @!visibility private
    module Native
      # @see Mustermann::Pattern#+
      # @!visibility private
      def +(other)
        other &&= Mustermann.new(other, type: :identity, **options)
        if (patterns = look_ahead(other)) && !patterns.empty?
          concat = (self + patterns.inject(:+))
          concat + other.patterns.slice(patterns.length..-1).inject(:+)
        else
          return super unless native = native_concat(other)
          self.class.new(native, **options)
        end
      end

      # @!visibility private
      def look_ahead(other)
        return unless other.is_a?(Concat)
        other.patterns.take_while(&method(:native_concat?))
      end

      # @!visibility private
      def native_concat(other)
        "#{self}#{other}" if native_concat?(other)
      end

      # @!visibility private
      def native_concat?(other)
        other.class == self.class and other.options == options
      end

      private :native_concat, :native_concat?
    end

    # Should not be used directly.
    # @!visibility private
    def initialize(*, **)
      super
      AST::Validation.validate(combined_ast) if respond_to? :expand
    end

    # @see Mustermann::Composite#operator
    # @return [Symbol] always :+
    def operator
      :+
    end

    # @see Mustermann::Pattern#===
    def ===(string)
      peek_size(string) == string.size
    end

    # @see Mustermann::Pattern#match
    def match(string)
      peeked = peek_match(string)
      peeked if peeked.to_s == string
    end

    # @see Mustermann::Pattern#params
    def params(string)
      params, size = peek_params(string)
      params if size == string.size
    end

    # @see Mustermann::Pattern#peek_size
    def peek_size(string)
      pump(string) { |p,s| p.peek_size(s) }
    end

    # @see Mustermann::Pattern#peek_match
    def peek_match(string)
      pump(string, initial: SimpleMatch.new) do |pattern, substring|
        return unless match = pattern.peek_match(substring)
        [match, match.to_s.size]
      end
    end

    # @see Mustermann::Pattern#peek_params
    def peek_params(string)
      pump(string, inject_with: :merge, with_size: true) { |p, s| p.peek_params(s) }
    end

    # (see Mustermann::Pattern#expand)
    def expand(behavior = nil, values = {})
      raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
      @expander ||= Mustermann::Expander.new(self) { combined_ast }
      @expander.expand(behavior, values)
    end

    # (see Mustermann::Pattern#to_templates)
    def to_templates
      raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
      @to_templates ||= patterns.inject(['']) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq
    end

    # @!visibility private
    def respond_to_special?(method)
      method = :to_ast if method.to_sym == :expand
      patterns.all? { |p| p.respond_to?(method) }
    end

    # used to generate results for various methods by scanning through an input string
    # @!visibility private
    def pump(string, inject_with: :+, initial: nil, with_size: false)
      substring = string
      results   = Array(initial)

      patterns.each do |pattern|
        result, size = yield(pattern, substring)
        return unless result
        results << result
        size    ||= result
        substring = substring[size..-1]
      end

      results = results.inject(inject_with)
      with_size ? [results, string.size - substring.size] : results
    end

    # generates one big AST from all patterns
    # will not check if patterns support AST generation
    # @!visibility private
    def combined_ast
      payload = patterns.map { |p| AST::Node[:group].new(p.to_ast.payload) }
      AST::Node[:root].new(payload)
    end

    private :combined_ast, :pump
  end
end