somleng/somleng-adhearsion

View on GitHub
components/app/app/models/twiml/connect_verb.rb

Summary

Maintainability
A
0 mins
Test Coverage
require_relative "twiml_node"

module TwiML
  class ConnectVerb < TwiMLNode
    class StreamNoun < TwiMLNode
      class Parser < TwiML::NodeParser
        VALID_NOUNS = [ "Parameter" ].freeze

        attr_reader :allow_insecure_urls

        def initialize(**)
          super
          @allow_insecure_urls = options.fetch(:allow_insecure_urls, false)
        end

        def parse(node)
          node_options = super
          node_options[:url] = attributes.fetch("url")
          node_options[:parameters] = build_parameters
          node_options
        end

        private

        def valid?
          validate_attributes && validate_nested_nouns
          super
        end

        def validate_attributes
          url_scheme = parse_url_scheme(attributes["url"])
          return true if url_scheme == "wss"
          return true if allow_insecure_urls && url_scheme == "ws"

          errors.add("<Stream> must contain a valid wss 'url' attribute")
          false
        end

        def validate_nested_nouns
          return true if nested_nodes.all? { |nested_node| VALID_NOUNS.include?(nested_node.name) }

          errors.add("<Stream> must only contain <Parameter> nouns")
          false
        end

        def build_parameters
          nested_nodes.each_with_object({}) do |nested_node, result|
            parameter_noun = ParameterNoun.parse(nested_node)
            result[parameter_noun.name] = parameter_noun.value
          end
        end

        def parse_url_scheme(url)
          URI(url.to_s).scheme
        end
      end

      class << self
        def parse(node, **options)
          super(node, parser: Parser.new(**options))
        end
      end

      attr_reader :url, :parameters

      def initialize(url:, parameters:, **options)
        super(**options)
        @url = url
        @parameters = parameters
      end
    end

    class ParameterNoun < TwiMLNode
      class Parser < TwiML::NodeParser
        def parse(node)
          node_options = super
          node_options[:name] = attributes.fetch("name")
          node_options[:value] = attributes.fetch("value")
          node_options
        end

        private

        def valid?
          validate_attributes
          super
        end

        def validate_attributes
          return if noun.attributes["name"].present? && noun.attributes["value"].present?

          errors.add("<Parameter> must have a 'name' and 'value' attribute")
        end

        def noun
          @noun ||= TwiMLNode.parse(node)
        end
      end

      class << self
        def parse(node)
          super(node, parser: Parser.new)
        end
      end

      attr_reader :name, :value

      def initialize(name:, value:, **options)
        super(options)
        @name = name
        @value = value
      end
    end

    class Parser < TwiML::NodeParser
      VALID_NOUNS = [ "Stream" ].freeze

      def parse(node)
        super.merge(stream_noun:)
      end

      private

      def valid?
        validate_nested_nouns

        super
      end

      def validate_nested_nouns
        return if nested_nodes.one? && VALID_NOUNS.include?(nested_nodes.first.name)

        valid_nouns = VALID_NOUNS.map { |noun| "<#{noun}>" }.join(", ")
        errors.add("<Connect> must contain exactly one of the following nouns: #{valid_nouns}")
      end

      def stream_noun
        @stream_noun ||= StreamNoun.parse(nested_nodes.first, **options)
      end
    end

    class << self
      def parse(node, **options)
        super(node, parser: Parser.new(**options))
      end
    end

    attr_reader :stream_noun

    def initialize(stream_noun:, **options)
      super(**options)
      @stream_noun = stream_noun
    end
  end
end