lib/whois/parser.rb in whois-parser-0.0.1 vs lib/whois/parser.rb in whois-parser-1.0.0.pre.beta1

- old
+ new

@@ -1,7 +1,411 @@ -require "whois/parser/version" +#-- +# Ruby Whois +# +# An intelligent pure Ruby WHOIS client and parser. +# +# Copyright (c) 2009-2015 Simone Carletti <weppos@weppos.net> +#++ + +require 'whois' +require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/time/calculations' +require_relative 'parser/version' +require_relative 'parser/errors' +require_relative 'parsers' + + +# The parsing controller that stays behind the {Whois::Record}. +# +# It provides object-oriented access to a WHOIS response. +# The list of properties and methods is available +# in the following constants: +# +# * {Whois::Parser::METHODS} +# * {Whois::Parser::PROPERTIES} +# module Whois - module Parser - # Your code goes here... + class Parser + + METHODS = [ + :contacts, + :changed?, :unchanged?, + # :response_incomplete?, :response_throttled?, :response_unavailable?, + # :referral_whois, :referral_url, + ] + + PROPERTIES = [ + :disclaimer, + :domain, :domain_id, + :status, :available?, :registered?, + :created_on, :updated_on, :expires_on, + :registrar, + :registrant_contacts, :admin_contacts, :technical_contacts, + :nameservers, + ] + + PROPERTY_STATE_NOT_IMPLEMENTED = :not_implemented + PROPERTY_STATE_NOT_SUPPORTED = :not_supported + PROPERTY_STATE_SUPPORTED = :supported + + + # Returns the proper parser instance for given <tt>part</tt>. + # The parser class is selected according to the + # value of the <tt>#host</tt> attribute for given <tt>part</tt>. + # + # @param [Whois::Record::Part] part The part to get the parser for. + # + # @return [Whois::Parsers::Base] + # An instance of the specific parser for given part. + # The instance is expected to be a child of {Whois::Parsers::Base}. + # + # @example + # + # # Parser for a known host + # Parser.parser_for("whois.example.com") + # # => #<Whois::Parsers::WhoisExampleCom> + # + # # Parser for an unknown host + # Parser.parser_for("missing.example.com") + # # => #<Whois::Parsers::Blank> + # + def self.parser_for(part) + parser_klass(part.host).new(part) + rescue LoadError + Parsers.const_defined?("Blank") || autoload("blank") + Parsers::Blank.new(part) + end + + # Detects the proper parser class according to given <tt>host</tt> + # and returns the class constant. + # + # This method autoloads missing parser classes. If you want to define + # a custom parser, simple make sure the class is loaded in the Ruby + # environment before this method is called. + # + # @param [String] host The server host. + # + # @return [Class] The instance of Class representing the parser Class + # corresponding to <tt>host</tt>. If <tt>host</tt> doesn't have + # a specific parser implementation, then returns + # the {Whois::Parsers::Blank} {Class}. + # The {Class} is expected to be a child of {Whois::Parsers::Base}. + # @raises LoadError If the class is not found. + # + # @example + # + # Parser.parser_klass("whois.example.com") + # # => Whois::Parsers::WhoisExampleCom + # + def self.parser_klass(host) + name = host_to_parser(host) + Parsers.const_defined?(name) || autoload(host) + Parsers.const_get(name) + end + + # Converts <tt>host</tt> to the corresponding parser class name. + # + # @param [String] host The server host. + # @return [String] The class name. + # + # @example + # + # Parser.host_to_parser("whois.nic.it") + # # => "WhoisNicIt" + # + # Parser.host_to_parser("whois.nic-info.it") + # # => "WhoisNicInfoIt" + # + def self.host_to_parser(host) + host.to_s.downcase. + gsub(/[.-]/, '_'). + gsub(/(?:^|_)(.)/) { $1.upcase }. + gsub(/\A(\d+)\z/) { "Host#{$1}" } + end + + # Requires the file at <tt>whois/parsers/#{name}</tt>. + # + # @param [String] name The file name to load. + # + # @return [void] + # + def self.autoload(name) + require "whois/parsers/#{name}" + end + + + # @return [Whois::Record] The record referenced by this parser. + attr_reader :record + + + # Initializes and return a new parser from +record+. + # + # @param [Whois::Record] record + # + def initialize(record) + @record = record + end + + # Checks if this class respond to given method. + # + # Overrides the default implementation to add support + # for {PROPERTIES} and {METHODS}. + # + # @return [Boolean] + def respond_to?(symbol, include_private = false) + respond_to_parser_method?(symbol) || super + end + + + # Returns an array with all host-specific parsers initialized for the parts + # contained into this parser. + # The array is lazy-initialized. + # + # @return [Array<Whois::Parsers::Base>] + # + def parsers + @parsers ||= init_parsers + end + + # Checks if the <tt>property</tt> passed as symbol + # is supported in any of the parsers. + # + # @return [Boolean] + # + # @see Whois::Parsers::Base.property_supported? + # + def property_any_supported?(property) + parsers.any? { |parser| parser.property_supported?(property) } + end + + # Checks if the <tt>property</tt> passed as symbol + # is "not implemented" in any of the parsers. + # + # @return [Boolean] + # + def property_any_not_implemented?(property) + parsers.any? { |parser| parser.class.property_state?(property, Whois::Parser::PROPERTY_STATE_NOT_IMPLEMENTED) } + end + + + # @!group Methods + + # Collects and returns all the contacts from all the record parts. + # + # @return [Array<Whois::Record::Contact>] + # + # @see Whois::Record#contacts + # @see Whois::Parsers::Base#contacts + # + def contacts + parsers.map(&:contacts).flatten + end + + # @!endgroup + + + # @!group Response + + # Loop through all the record parts to check + # if at least one part changed. + # + # @param [Whois::Parser] other The other parser instance to compare. + # @return [Boolean] + # + # @see Whois::Record#changed? + # @see Whois::Parsers::Base#changed? + # + def changed?(other) + !unchanged?(other) + end + + # The opposite of {#changed?}. + # + # @param [Whois::Parser] other The other parser instance to compare. + # @return [Boolean] + # + # @see Whois::Record#unchanged? + # @see Whois::Parsers::Base#unchanged? + # + def unchanged?(other) + unless other.is_a?(self.class) + raise(ArgumentError, "Can't compare `#{self.class}' with `#{other.class}'") + end + + equal?(other) || + parsers.size == other.parsers.size && all_in_parallel?(parsers, other.parsers) { |one, two| one.unchanged?(two) } + end + + + # Loop through all the parts to check if at least + # one part is an incomplete response. + # + # @return [Boolean] + # + # @see Whois::Record#response_incomplete? + # @see Whois::Parsers::Base#response_incomplete? + # + def response_incomplete? + any_is?(parsers, :response_incomplete?) + end + + # Loop through all the parts to check if at least + # one part is a throttle response. + # + # @return [Boolean] + # + # @see Whois::Record#response_throttled? + # @see Whois::Parsers::Base#response_throttled? + # + def response_throttled? + any_is?(parsers, :response_throttled?) + end + + # Loop through all the parts to check if at least + # one part is an unavailable response. + # + # @return [Boolean] + # + # @see Whois::Record#response_unavailable? + # @see Whois::Parsers::Base#response_unavailable? + # + def response_unavailable? + any_is?(parsers, :response_unavailable?) + end + + # @!endgroup + + + private + + # @api private + def self.define_property_method(method) + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + delegate_property_to_parsers(:#{method}, *args, &block) + end + RUBY + end + + # @api private + def self.define_method_method(method) + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + delegate_method_to_parsers(:#{method}, *args, &block) + end + RUBY + end + + def respond_to_parser_method?(symbol) + PROPERTIES.include?(symbol) || METHODS.include?(symbol) + end + + def method_missing(method, *args, &block) + if PROPERTIES.include?(method) + self.class.define_property_method(method) + send(method, *args, &block) + elsif METHODS.include?(method) + self.class.define_method_method(method) + send(method, *args, &block) + else + super + end + end + + def delegate_property_to_parsers(method, *args, &block) + if parsers.empty? + raise ParserError, "Unable to select a parser because the Record is empty" + elsif (parser = select_parser { |p| p.class.property_state?(method, PROPERTY_STATE_SUPPORTED) }) + parser.send(method, *args, &block) + elsif (parser = select_parser { |p| p.class.property_state?(method, PROPERTY_STATE_NOT_SUPPORTED) }) + parser.send(method, *args, &block) + else + raise AttributeNotImplemented, "Unable to find a parser for property `#{method}'" + end + end + + def delegate_method_to_parsers(method, *args, &block) + if parsers.empty? + raise ParserError, "Unable to select a parser because the Record is empty" + elsif (parser = select_parser { |p| p.respond_to?(method) }) + parser.send(method, *args, &block) + else + nil + end + end + + # Loops through all record parts, for each part + # tries to guess the appropriate parser object whenever available, + # and returns the final array of server-specific parsers. + # + # Parsers are initialized in reverse order for performance reason. + # + # @return [Array<Class>] An array of Class, + # where each item is the parts reverse-N specific parser {Class}. + # Each {Class} is expected to be a child of {Whois::Parsers::Base}. + # + # @example + # + # parser.parts + # # => [whois.foo.com, whois.bar.com] + # + # parser.parsers + # # => [Whois::Parsers::WhoisBarCom, Whois::Parsers::WhoisFooCom] + # + # @api private + def init_parsers + record.parts.reverse.map { |part| self.class.parser_for(part) } + end + + # Selects the first parser in {#parsers} where blocks evaluates to true. + # + # @return [Whois::Parsers::Base] + # The parser for which the block returns true. + # @return [nil] + # If the parser wasn't found. + # + # @yield [parser] + # + # @example + # + # select_parser { |parser| parser.class.property_state?(:nameserver, :any) } + # # => #<Whois::Parsers::WhoisExampleCom> + # select_parser { |parser| parser.class.property_state?(:nameservers, PROPERTY_STATE_SUPPORTED) } + # # => nil + # + # @api private + def select_parser(&block) + parsers.each do |parser| + return parser if block.call(parser) + end + nil + end + + # @api private + def all_in_parallel?(*args) + count = args.first.size + index = 0 + + while index < count + return false unless yield(*args.map { |arg| arg[index] }) + index += 1 + end + true + end + + # @api private + def any_is?(collection, symbol) + collection.any? { |item| item.is(symbol) } + end + end end + +require_relative 'parsers' +require_relative 'parsers/base' +require_relative 'parser_extensions' +