#!/usr/bin/env ruby

###
### $Rev: 48 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

#--begin of require 'kwalify'
###
### $Rev: 48 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###


module Kwalify

   RELEASE = ("$Release: 0.6.1 $" =~ /[.\d]+/) && $&

end

#--begin of require 'kwalify/types'
###
### $Rev: 51 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

require 'date'


module Kwalify
   module Boolean   # :nodoc:
   end
end
class TrueClass   # :nodoc:
   include Kwalify::Boolean
end
class FalseClass   # :nodoc:
   include Kwalify::Boolean
end
#module Boolean; end
#class TrueClass
#   include Boolean
#end
#class FalseClass
#   include Boolean
#end


module Kwalify
   module Text   # :nodoc:
   end
end
class String   # :nodoc:
   include Kwalify::Text
end
class Numeric   # :nodoc:
   include Kwalify::Text
end
#module Text; end
#class String
#   include Text
#end
#class Numeric
#   include Text
#end


module Kwalify
   module Scalar   # :nodoc:
   end
end
class String   # :nodoc:
   include Kwalify::Scalar
end
class Numeric   # :nodoc:
   include Kwalify::Scalar
end
class Date   # :nodoc:
   include Kwalify::Scalar
end
class Time   # :nodoc:
   include Kwalify::Scalar
end
class TrueClass   # :nodoc:
   include Kwalify::Scalar
end
class FalseClass   # :nodoc:
   include Kwalify::Scalar
end
class NilClass   # :nodoc:
   include Kwalify::Scalar
end
module Kwalify
   module Text   # :nodoc:
      include Kwalify::Scalar
   end
end


module Kwalify


   module Types


      DEFAULT_TYPE = "str"           ## use "str" as default of @type

      @@type_table = {
         "seq"       => Array,
         "map"       => Hash,
         "str"       => String,
         #"string"    => String,
         "text"      => Text,
         "int"       => Integer,
         #"integer"   => Integer,
         "float"     => Float,
         "number"    => Numeric,
         #"numeric"   => Numeric,
         "date"      => Date,
         "time"      => Time,
         "timestamp" => Time,
         "bool"      => Boolean,
         #"boolean"   => Boolean,
         #"object"    => Object,
         "any"       => Object,
         "scalar"    => Scalar,
      }

      def self.type_table
         return @@type_table
      end

      def self.type_class(type)
         klass = @@type_table[type]
         #assert_error('type=#{type.inspect}') unless klass
         return klass
      end

      def self.get_type_class(type)
         return type_class(type)
      end



      #--
      #def collection_class?(klass)
      #   return klass.is_a?(Array) || klass.is_a?(Hash)
      #end
      #
      #def scalar_class?(klass)
      #   return !klass.is_a?(Array) && !klass.is_a?(Hash) && klass != Object
      #end

      def collection?(val)
         return val.is_a?(Array) || val.is_a?(Hash)
      end

      def scalar?(val)
         return !val.is_a?(Array) && !val.is_a?(Hash) && val.class != Object
      end

      def collection_type?(type)
         return type == 'seq' || type == 'map'
      end

      def scalar_type?(type)
         return type != 'seq' && type != 'map' && type == 'any'
      end

      module_function 'collection?', 'scalar?', 'collection_type?', 'scalar_type?'
   end

   extend Types

end
#--end of require 'kwalify/types'
#--begin of require 'kwalify/messages'
###
### $Rev: 51 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

module Kwalify

   @@messages = {}

   def self.msg(key)
      return @@messages[key]
   end



   @@messages[:command_help] = <<END
kwalify - tiny schema validator for YAML and JSON
Usage1: kwalify [..options..] -f schema.yaml doc.yaml [doc2.yaml ...]
Usage2: kwalify [..options..] -m schema.yaml [schema2.yaml ...]
Usage3: kwalify [..options..] -a action -f schema.yaml [schema2.yaml ...]
  -h, --help      :  help
  -v              :  version
  -s              :  silent
  -f schema.yaml  :  schema definition file
  -m              :  meta-validation mode
  -t              :  expand tab characters
  -l              :  show linenumber when errored (experimental)
  -E              :  show errors in emacs-style (implies '-l')
  -a action       :  action ('genclass-ruby' or 'genclass-java')
                     (use '-ha action' option to show action properites)
END
#  -I path         :  path for template of action



   ##----- begin
   # filename: lib/kwalify/main.rb
   @@messages[:command_option_actionnoschema] = "schema filename is not specified."
   @@messages[:command_option_noaction] = "command-line option '-f' or '-m' required."
   @@messages[:command_option_notemplate] = "%s: invalid action (template not found).\n"
   @@messages[:schema_empty]         = "%s: empty schema.\n"
   @@messages[:validation_empty]     = "%s#%d: empty.\n"
   @@messages[:validation_valid]     = "%s#%d: valid.\n"
   @@messages[:validation_invalid]   = "%s#%d: INVALID\n"
   @@messages[:command_option_noschema] = "-%s: schema filename required."
   @@messages[:command_option_noaction] = "-%s: action required."
   @@messages[:command_option_notpath] = "-%s: template path required."
   @@messages[:command_property_invalid] = "%s: invalid property."
   @@messages[:command_option_invalid] = "-%s: invalid command option."
   # --
   # filename: lib/kwalify/rule.rb
   @@messages[:schema_notmap]        = "schema definition is not a mapping."
   @@messages[:key_unknown]          = "unknown key."
   @@messages[:type_notstr]          = "not a string."
   @@messages[:type_unknown]         = "unknown type."
   @@messages[:classname_notmap]     = "available only with map type."
   @@messages[:required_notbool]     = "not a boolean."
   @@messages[:pattern_notstr]       = "not a string (or regexp)"
   @@messages[:pattern_notmatch]     = "should be '/..../'."
   @@messages[:pattern_syntaxerr]    = "has regexp error."
   @@messages[:enum_notseq]          = "not a sequence."
   @@messages[:enum_notscalar]       = "not available with seq or map."
   @@messages[:enum_type_unmatch]    = "%s type expected."
   @@messages[:enum_duplicate]       = "duplicated enum value."
   @@messages[:assert_notstr]        = "not a string."
   @@messages[:assert_noval]         = "'val' is not used."
   @@messages[:assert_syntaxerr]     = "expression syntax error."
   @@messages[:range_notmap]         = "not a mapping."
   @@messages[:range_notscalar]      = "is available only with scalar type."
   @@messages[:range_type_unmatch]   = "not a %s."
   @@messages[:range_undefined]      = "undefined key."
   @@messages[:range_twomax]         = "both 'max' and 'max-ex' are not available at once."
   @@messages[:range_twomin]         = "both 'min' and 'min-ex' are not available at once."
   @@messages[:range_maxltmin]       = "max '%s' is less than min '%s'."
   @@messages[:range_maxleminex]     = "max '%s' is less than or equal to min-ex '%s'."
   @@messages[:range_maxexlemin]     = "max-ex '%s' is less than or equal to min '%s'."
   @@messages[:range_maxexleminex]   = "max-ex '%s' is less than or equal to min-ex '%s'."
   @@messages[:length_notmap]        = "not a mapping."
   @@messages[:length_nottext]       = "is available only with string or text."
   @@messages[:length_notint]        = "not an integer."
   @@messages[:length_undefined]     = "undefined key."
   @@messages[:length_twomax]        = "both 'max' and 'max-ex' are not available at once."
   @@messages[:length_twomin]        = "both 'min' and 'min-ex' are not available at once."
   @@messages[:length_maxltmin]      = "max '%s' is less than min '%s'."
   @@messages[:length_maxleminex]    = "max '%s' is less than or equal to min-ex '%s'."
   @@messages[:length_maxexlemin]    = "max-ex '%s' is less than or equal to min '%s'."
   @@messages[:length_maxexleminex]  = "max-ex '%s' is less than or equal to min-ex '%s'."
   @@messages[:ident_notbool]        = "not a boolean."
   @@messages[:ident_notscalar]      = "is available only with a scalar type."
   @@messages[:ident_onroot]         = "is not available on root element."
   @@messages[:ident_notmap]         = "is available only with an element of mapping."
   @@messages[:unique_notbool]       = "not a boolean."
   @@messages[:unique_notscalar]     = "is available only with a scalar type."
   @@messages[:unique_onroot]        = "is not available on root element."
   @@messages[:sequence_notseq]      = "not a sequence."
   @@messages[:sequence_noelem]      = "required one element."
   @@messages[:sequence_toomany]     = "required just one element."
   @@messages[:mapping_notmap]       = "not a mapping."
   @@messages[:mapping_noelem]       = "required at least one element."
   @@messages[:seq_nosequence]       = "type 'seq' requires 'sequence:'."
   @@messages[:seq_conflict]         = "not available with sequence."
   @@messages[:map_nomapping]        = "type 'map' requires 'mapping:'."
   @@messages[:map_conflict]         = "not available with mapping."
   @@messages[:scalar_conflict]      = "not available with scalar type."
   @@messages[:enum_conflict]        = "not available with 'enum:'."
   # --
   # filename: lib/kwalify/validator.rb
   @@messages[:required_novalue]     = "value required but none."
   @@messages[:type_unmatch]         = "not a %s."
   @@messages[:assert_failed]        = "assertion expression failed (%s)."
   @@messages[:enum_notexist]        = "invalid %s value."
   @@messages[:pattern_unmatch]      = "not matched to pattern %s."
   @@messages[:range_toolarge]       = "too large (> max %s)."
   @@messages[:range_toosmall]       = "too small (< min %s)."
   @@messages[:range_toolargeex]     = "too large (>= max %s)."
   @@messages[:range_toosmallex]     = "too small (<= min %s)."
   @@messages[:length_toolong]       = "too long (length %d > max %d)."
   @@messages[:length_tooshort]      = "too short (length %d < min %d)."
   @@messages[:length_toolongex]     = "too long (length %d >= max %d)."
   @@messages[:length_tooshortex]    = "too short (length %d <= min %d)."
   @@messages[:value_notunique]      = "is already used at '%s'."
   @@messages[:required_nokey]       = "key '%s:' is required."
   @@messages[:key_undefined]        = "key '%s' is undefined."
   # --
   # filename: lib/kwalify/yaml-parser.rb
   @@messages[:flow_hastail]         = "flow style sequence is closed but got '%s'."
   @@messages[:flow_eof]             = "found EOF when parsing flow style."
   @@messages[:flow_noseqitem]       = "sequence item required (or last comma is extra)."
   @@messages[:flow_seqnotclosed]    = "flow style sequence requires ']'."
   @@messages[:flow_mapnoitem]       = "mapping item required (or last comma is extra)."
   @@messages[:flow_mapnotclosed]    = "flow style mapping requires '}'."
   @@messages[:flow_nocolon]         = "':' expected but got '%s'."
   @@messages[:anchor_duplicated]    = "anchor '%s' is already used."
   @@messages[:alias_extradata]      = "alias cannot take any data."
   @@messages[:anchor_notfound]      = "anchor '%s' not found"
   @@messages[:sequence_noitem]      = "sequence item is expected."
   @@messages[:sequence_badindent]   = "illegal indent of sequence."
   @@messages[:mapping_noitem]       = "mapping item is expected."
   @@messages[:mapping_badindent]    = "illegal indent of mapping."
   # --
   ##----- end




   @@words = {}

   def self.word(key)
      return @@words[key] || key
   end

   @@words['str']  = 'string'
   @@words['int']  = 'integer'
   @@words['bool'] = 'boolean'
   @@words['seq']  = 'sequence'
   @@words['map']  = 'mapping'

end
#--end of require 'kwalify/messages'
#--begin of require 'kwalify/errors'
###
### $Rev: 48 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

#--already included require 'kwalify/messages'

module Kwalify

   class KwalifyError < StandardError
   end


   class AssertionError < KwalifyError
      def initialize(msg)
         super("*** assertion error: " + msg)
      end
   end


   class BaseError < KwalifyError
      def initialize(message="", path=nil, value=nil, rule=nil, error_symbol=nil)
         super(message)
         @path        = path
         @rule         = rule
         @value        = value
         @error_symbol = error_symbol
      end
      attr_reader :error_symbol, :rule, :path, :value
      attr_accessor :linenum

      def path
         return @path == '' ? "/" : @path
      end

      alias :_to_s :to_s

      def message
         _to_s
      end

      def to_s
         return "[#{path()}] #{message()}"
      end

      def <=>(ex)
        return @linenum <=> ex.linenum
      end
   end


   class SchemaError < BaseError
      def initialize(message="", path=nil, rule=nil, value=nil, error_symbol=nil)
         super(message, path, rule, value, error_symbol)
      end
   end


   class ValidationError < BaseError
      def initialize(message="", path=nil, rule=nil, value=nil, error_symbol=nil)
         super(message, path, rule, value, error_symbol)
      end
   end


   class YamlSyntaxError < KwalifyError
      def initialize(msg, linenum, error_symbol)
         super("line #{linenum}: #{msg}")
         @linenum = linenum
         @error_symbol
      end
      attr_accessor :linenum, :error_symbol
   end


   module ErrorHelper

      def assert_error(message="")
         raise AssertionError.new(message)
      end

      def validate_error(error_symbol, rule, path, val, args=nil)
         msg = _build_message(error_symbol, val, args);
         return ValidationError.new(msg, path, val, rule, error_symbol)
      end

      def schema_error(error_symbol, rule, path, val, args=nil)
         msg = _build_message(error_symbol, val, args);
         return SchemaError.new(msg, path, val, rule, error_symbol)
      end

      def _build_message(message_key, val, args)
         msg = Kwalify.msg(message_key)
         assert_error("message_key=#{message_key.inspect}") unless msg
         msg = msg % args if args
         msg = "'#{val.to_s.gsub(/\n/, '\n')}': #{msg}" if val != nil && Types.scalar?(val)
         return msg;
      end

   end

   extend ErrorHelper

end
#--end of require 'kwalify/errors'
#--begin of require 'kwalify/rule'
###
### $Rev: 51 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

#--already included require 'kwalify/messages'
#--already included require 'kwalify/errors'
#--already included require 'kwalify/types'

module Kwalify

   class Rule
      include Kwalify::ErrorHelper

      def initialize(hash=nil, parent=nil)
         init(hash, "", {}) if hash
         @parent = parent
      end

      attr_accessor :parent
      #attr_reader   :id
      attr_reader :name
      attr_reader :desc
      attr_reader :enum
      attr_reader :required
      attr_reader :type
      attr_reader :type_class
      attr_reader :pattern
      attr_reader :regexp
      attr_reader :sequence
      attr_reader :mapping
      attr_reader :assert
      attr_reader :assert_proc
      attr_reader :range
      attr_reader :length
      attr_reader :ident
      attr_reader :unique
      attr_reader :classname

      def init(hash, path="", rule_table={})
         unless hash.is_a?(Hash)
            #* key=:schema_notmap  msg="schema definition is not a mapping."
            raise Kwalify.schema_error(:schema_notmap, nil, (!path || path.empty? ? "/" : path), nil)
         end
         rule = self
         rule_table[hash.__id__] = rule
         ## 'type:' entry
         _init_type_value(hash['type'], rule, path)
         ## other entries
         hash.each do |key, val|
            code = key.intern
            curr_path = "#{path}/#{key}"
            case code
            #when "id"
            #   @id       = val
            when :type      ;  # done
            when :name      ;  _init_name_value(    val, rule, path)
            when :desc      ;  _init_desc_value(    val, rule, path)
            when :required  ;  _init_required_value(val, rule, path)
            when :pattern   ;  _init_pattern_value( val, rule, path)
            when :enum      ;  _init_enum_value(    val, rule, path)
            when :assert    ;  _init_assert_value(  val, rule, path)
            when :range     ;  _init_range_value(   val, rule, path)
            when :length    ;  _init_length_value(  val, rule, path)
            when :ident     ;  _init_ident_value(   val, rule, path)
            when :unique    ;  _init_unique_value(  val, rule, path)
            when :sequence  ;  _init_sequence_value(val, rule, path, rule_table)
            when :mapping   ;  _init_mapping_value( val, rule, path, rule_table)
            when :classname ;  _init_classname_value(val, rule, path)
            else
               #* key=:key_unknown  msg="unknown key."
               raise schema_error(:key_unknown, rule, curr_path, "#{key}:")
            end
         end
         check_confliction(hash, rule, path)
         return self
      end # end of def init


      private


      def _init_type_value(val, rule, path)
         @type = val
         @type = Types::DEFAULT_TYPE if @type == nil
         unless @type.is_a?(String)
            #* key=:type_notstr  msg="not a string."
            raise schema_error(:type_notstr, rule, "#{path}/type", @type.to_s)
         end
         @type_class = Types.type_class(@type)
         #if @type_class == nil
         #   begin
         #      @type_class = Kernel.const_get(@type)
         #   rescue NameError
         #   end
         #end
         unless @type_class
            #* key=:type_unknown  msg="unknown type."
            raise schema_error(:type_unknown, rule, "#{path}/type", @type.to_s)
         end
      end


      def _init_classname_value(val, rule, path)
         @classname = val
         unless @type == 'map'
            #* key=:classname_notmap  msg="available only with map type."
            raise schema_error(:classname_notmap, rule, "#{path}/classname", 'classname:')
         end
      end


      def _init_name_value(val, rule, path)
         @name = val
      end


      def _init_desc_value(val, rule, path)
         @desc = val
      end


      def _init_required_value(val, rule, path)
         @required = val
         unless val.is_a?(Boolean)  #|| val == nil
            #* key=:required_notbool  msg="not a boolean."
            raise schema_error(:required_notbool, rule, "#{path}/required", val)
         end
      end

      def _init_pattern_value(val, rule, path)
         @pattern = val
         unless val.is_a?(String) || val.is_a?(Regexp)
            #* key=:pattern_notstr  msg="not a string (or regexp)"
            raise schema_error(:pattern_notstr, rule, "#{path}/pattern", val)
         end
         unless val =~ /\A\/(.*)\/([mi]?[mi]?)\z/
            #* key=:pattern_notmatch  msg="should be '/..../'."
            raise schema_error(:pattern_notmatch, rule, "#{path}/pattern", val)
         end
         pat = $1; opt = $2
         flag = 0
         flag += Regexp::IGNORECASE if opt.include?("i")
         flag += Regexp::MULTILINE  if opt.include?("m")
         begin
            @regexp = Regexp.compile(pat, flag)
         rescue RegexpError => ex
            #* key=:pattern_syntaxerr  msg="has regexp error."
            raise schema_error(:pattern_syntaxerr, rule, "#{path}/pattern", val)
         end
      end


      def _init_enum_value(val, rule, path)
         @enum = val
         unless val.is_a?(Array)
            #* key=:enum_notseq  msg="not a sequence."
            raise schema_error(:enum_notseq, rule, "#{path}/enum", val)
         end
         if Types.collection_type?(@type)   # unless Kwalify.scalar_class?(@type_class)
            #* key=:enum_notscalar  msg="not available with seq or map."
            raise schema_error(:enum_notscalar, rule, path, 'enum:')
         end
         elem_table = {}
         @enum.each do |elem|
            unless elem.is_a?(@type_class)
               #* key=:enum_type_unmatch  msg="%s type expected."
               raise schema_error(:enum_type_unmatch, rule, "#{path}/enum", elem, [Kwalify.word(@type)])
            end
            if elem_table[elem]
               #* key=:enum_duplicate  msg="duplicated enum value."
               raise schema_error(:enum_duplicate, rule, "#{path}/enum", elem.to_s)
            end
            elem_table[elem] = true
         end
      end


      def _init_assert_value(val, rule, path)
         @assert = val
         unless val.is_a?(String)
            #* key=:assert_notstr  msg="not a string."
            raise schema_error(:assert_notstr, rule, "#{path}/assert", val)
         end
         unless val =~ /\bval\b/
            #* key=:assert_noval  msg="'val' is not used."
            raise schema_error(:assert_noval, rule, "#{path}/assert", val)
         end
         begin
            @assert_proc = eval "proc { |val| #{val} }"
         rescue SyntaxError => ex
            #* key=:assert_syntaxerr  msg="expression syntax error."
            raise schema_error(:assert_syntaxerr, rule, "#{path}/assert", val)
         end
      end


      def _init_range_value(val, rule, path)
         @range = val
         curr_path = "#{path}/range"
         unless val.is_a?(Hash)
            #* key=:range_notmap  msg="not a mapping."
            raise schema_error(:range_notmap, rule, curr_path, val)
         end
         if Types.collection_type?(@type) || @type == 'bool'
            #* key=:range_notscalar  msg="is available only with scalar type."
            raise schema_error(:range_notscalar, rule, path, 'range:')
         end
         val.each do |k, v|
            case k
            when 'max', 'min', 'max-ex', 'min-ex'
               unless v.is_a?(@type_class)
                  typename = Kwalify.word(@type) || @type
                  #* key=:range_type_unmatch  msg="not a %s."
                  raise schema_error(:range_type_unmatch, rule, "#{curr_path}/#{k}", v, [typename])
               end
            else
               #* key=:range_undefined  msg="undefined key."
               raise schema_error(:range_undefined, rule, "#{curr_path}/#{k}", "#{k}:")
            end
         end
         if val.key?('max') && val.key?('max-ex')
            #* key=:range_twomax  msg="both 'max' and 'max-ex' are not available at once."
            raise schema_error(:range_twomax, rule, curr_path, nil)
         end
         if val.key?('min') && val.key?('min-ex')
            #* key=:range_twomin  msg="both 'min' and 'min-ex' are not available at once."
            raise schema_error(:range_twomin, rule, curr_path, nil)
         end
         max, min, max_ex, min_ex = val['max'], val['min'], val['max-ex'], val['min-ex']
         if max
            if min && max < min
               #* key=:range_maxltmin  msg="max '%s' is less than min '%s'."
               raise validate_error(:range_maxltmin, rule, curr_path, nil, [max, min])
            elsif min_ex && max <= min_ex
               #* key=:range_maxleminex  msg="max '%s' is less than or equal to min-ex '%s'."
               raise validate_error(:range_maxleminex, rule, curr_path, nil, [max, min_ex])
            end
         elsif max_ex
            if min && max_ex <= min
               #* key=:range_maxexlemin msg="max-ex '%s' is less than or equal to min '%s'."
               raise validate_error(:range_maxexlemin, rule, curr_path, nil, [max_ex, min])
            elsif min_ex && max_ex <= min_ex
               #* key=:range_maxexleminex msg="max-ex '%s' is less than or equal to min-ex '%s'."
               raise validate_error(:range_maxexleminex, rule, curr_path, nil, [max_ex, min_ex])
            end
         end
      end


      def _init_length_value(val, rule, path)
         @length = val
         curr_path = "#{path}/length"
         unless val.is_a?(Hash)
            #* key=:length_notmap  msg="not a mapping."
            raise schema_error(:length_notmap, rule, curr_path, val)
         end
         unless @type == 'str' || @type == 'text'
            #* key=:length_nottext  msg="is available only with string or text."
            raise schema_error(:length_nottext, rule, path, 'length:')
         end
         val.each do |k, v|
            case k
            when 'max', 'min', 'max-ex', 'min-ex'
               unless v.is_a?(Integer)
                  #* key=:length_notint  msg="not an integer."
                  raise schema_error(:length_notint, rule, "#{curr_path}/#{k}", v)
               end
            else
               #* key=:length_undefined  msg="undefined key."
               raise schema_error(:length_undefined, rule, "#{curr_path}/#{k}", "#{k}:")
            end
         end
         if val.key?('max') && val.key?('max-ex')
            #* key=:length_twomax msg="both 'max' and 'max-ex' are not available at once."
            raise schema_error(:length_twomax, rule, curr_path, nil)
         end
         if val.key?('min') && val.key?('min-ex')
            #* key=:length_twomin msg="both 'min' and 'min-ex' are not available at once."
            raise schema_error(:length_twomin, rule, curr_path, nil)
         end
         max, min, max_ex, min_ex = val['max'], val['min'], val['max-ex'], val['min-ex']
         if max
            if min && max < min
               #* key=:length_maxltmin  msg="max '%s' is less than min '%s'."
               raise validate_error(:length_maxltmin, rule, curr_path, nil, [max, min])
            elsif min_ex && max <= min_ex
               #* key=:length_maxleminex  msg="max '%s' is less than or equal to min-ex '%s'."
               raise validate_error(:length_maxleminex, rule, curr_path, nil, [max, min_ex])
            end
         elsif max_ex
            if min && max_ex <= min
               #* key=:length_maxexlemin  msg="max-ex '%s' is less than or equal to min '%s'."
               raise validate_error(:length_maxexlemin, rule, curr_path, nil, [max_ex, min])
            elsif min_ex && max_ex <= min_ex
               #* key=:length_maxexleminex  msg="max-ex '%s' is less than or equal to min-ex '%s'."
               raise validate_error(:length_maxexleminex, rule, curr_path, nil, [max_ex, min_ex])
            end
         end
      end


      def _init_ident_value(val, rule, path)
         @ident = val
         @required = true
         unless val.is_a?(Boolean)
            #* key=:ident_notbool  msg="not a boolean."
            raise schema_error(:ident_notbool, rule, "#{path}/ident", val)
         end
         if @type == 'map' || @type == 'seq'
            #* key=:ident_notscalar  msg="is available only with a scalar type."
            raise schema_error(:ident_notscalar, rule, path, "ident:")
         end
         if path.empty?
            #* key=:ident_onroot  msg="is not available on root element."
            raise schema_error(:ident_onroot, rule, "/", "ident:")
         end
         unless @parent && @parent.type == 'map'
            #* key=:ident_notmap  msg="is available only with an element of mapping."
            raise schema_error(:ident_notmap, rule, path, "ident:")
         end
      end


      def _init_unique_value(val, rule, path)
         @unique = val
         unless val.is_a?(Boolean)
            #* key=:unique_notbool  msg="not a boolean."
            raise schema_error(:unique_notbool, rule, "#{path}/unique", val)
         end
         if @type == 'map' || @type == 'seq'
            #* key=:unique_notscalar  msg="is available only with a scalar type."
            raise schema_error(:unique_notscalar, rule, path, "unique:")
         end
         if path.empty?
            #* key=:unique_onroot  msg="is not available on root element."
            raise schema_error(:unique_onroot, rule, "/", "unique:")
         end
      end


      def _init_sequence_value(val, rule, path, rule_table)
         if val != nil && !val.is_a?(Array)
            #* key=:sequence_notseq  msg="not a sequence."
            raise schema_error(:sequence_notseq, rule, "#{path}/sequence", val)
         elsif val == nil || val.empty?
            #* key=:sequence_noelem  msg="required one element."
            raise schema_error(:sequence_noelem, rule, "#{path}/sequence", val)
         elsif val.length > 1
            #* key=:sequence_toomany  msg="required just one element."
            raise schema_error(:sequence_toomany, rule, "#{path}/sequence", val)
         else
            elem = val[0]
            elem ||= {}
            i = 0   # or 1?  *index*
            rule = rule_table[elem.__id__]
            rule ||= Rule.new(nil, self).init(elem, "#{path}/sequence/#{i}", rule_table)
            @sequence = [ rule ]
         end
      end


      def _init_mapping_value(val, rule, path, rule_table)
         if val != nil && !val.is_a?(Hash)
            #* key=:mapping_notmap  msg="not a mapping."
            raise schema_error(:mapping_notmap, rule, "#{path}/mapping", val)
         elsif val == nil || (val.empty? && !val.default)
            #* key=:mapping_noelem  msg="required at least one element."
            raise schema_error(:mapping_noelem, rule, "#{path}/mapping", val)
         else
            @mapping = {}
            if val.default
               elem = val.default  # hash
               rule = rule_table[elem.__id__]
               rule ||= Rule.new(nil, self).init(elem, "#{path}/mapping/=", rule_table)
               @mapping.default = rule
            end
            val.each do |k, v|
               ##* key=:key_duplicate  msg="key duplicated."
               #raise schema_error(:key_duplicate, rule, "#{path}/mapping", key) if @mapping.key?(key)
               v ||= {}
               rule = rule_table[v.__id__]
               rule ||= Rule.new(nil, self).init(v, "#{path}/mapping/#{k}", rule_table)
               if k == '='
                  @mapping.default = rule
               else
                  @mapping[k] = rule
               end
            end if val
         end
      end


      def check_confliction(hash, rule, path)
         if @type == 'seq'
            #* key=:seq_nosequence  msg="type 'seq' requires 'sequence:'."
            raise schema_error(:seq_nosequence, rule, path, nil) unless hash.key?('sequence')
            #* key=:seq_conflict  msg="not available with sequence."
            raise schema_error(:seq_conflict, rule, path, 'enum:')        if @enum
            raise schema_error(:seq_conflict, rule, path, 'pattern:')     if @pattern
            raise schema_error(:seq_conflict, rule, path, 'mapping:')     if @mapping
            raise schema_error(:seq_conflict, rule, path, 'range:')       if @range
            raise schema_error(:seq_conflict, rule, path, 'length:')      if @length
         elsif @type == 'map'
            #* key=:map_nomapping  msg="type 'map' requires 'mapping:'."
            raise schema_error(:map_nomapping, rule, path, nil)  unless hash.key?('mapping')
            #* key=:map_conflict  msg="not available with mapping."
            raise schema_error(:map_conflict, rule, path, 'enum:')        if @enum
            raise schema_error(:map_conflict, rule, path, 'pattern:')     if @pattern
            raise schema_error(:map_conflict, rule, path, 'sequence:')    if @sequence
            raise schema_error(:map_conflict, rule, path, 'range:')       if @range
            raise schema_error(:map_conflict, rule, path, 'length:')      if @length
         else
            #* key=:scalar_conflict  msg="not available with scalar type."
            raise schema_error(:scalar_conflict, rule, path, 'sequence:') if @sequence
            raise schema_error(:scalar_conflict, rule, path, 'mapping:')  if @mapping
            if @enum
               #* key=:enum_conflict  msg="not available with 'enum:'."
               raise schema_error(:enum_conflict, rule, path, 'range:')   if @range
               raise schema_error(:enum_conflict, rule, path, 'length:')  if @length
               raise schema_error(:enum_conflict, rule, path, 'pattern:') if @pattern
            end
         end
      end

      #def inspect()
      #   str = "";  level = 0;  done = {}
      #   _inspect(str, level, done)
      #   return str
      #end


      protected


      def _inspect(str="", level=0, done={})
         done[self.__id__] = true
         str << "  " * level << "name:      #{@name}\n"             if @name     != nil
         str << "  " * level << "desc:      #{@desc}\n"             if @desc     != nil
         str << "  " * level << "type:      #{@type}\n"             if @type     != nil
         str << "  " * level << "klass:     #{@type_class.name}\n"  if @type_class    != nil
         str << "  " * level << "required:  #{@required}\n"         if @required != nil
         str << "  " * level << "pattern:   #{@regexp.inspect}\n"   if @pattern  != nil
         str << "  " * level << "assert:    #{@assert}\n"           if @assert   != nil
         str << "  " * level << "ident:     #{@ident}\n"            if @ident    != nil
         str << "  " * level << "unique:    #{@unique}\n"           if @unique   != nil
         if @enum != nil
            str << "  " * level << "enum:\n"
            @enum.each do |item|
               str << "  " * (level+1) << "- #{item}\n"
            end
         end
         if @range != nil
            str << "  " * level
            str << "range:     { "
            colon = ""
            %w[max max-ex min min-ex].each do |key|
               val = @range[key]
               if val != nil
                  str << colon << "#{key}: #{val.inspect}"
                  colon = ", "
               end
            end
            str << " }\n"
         end
         if @length != nil
            str << "  " * level
            str << "length:     { "
            colon = ""
            %w[max max-ex min min-ex].each do |key|
               val = @length[key]
               if val != nil
                  str << colon << "#{key}: #{val.inspect}"
                  colon = ", "
               end
            end
            str << " }\n"
         end
         @sequence.each do |rule|
            if done[rule.__id__]
               str << "  " * (level+1) << "- ...\n"
            else
               str << "  " * (level+1) << "- \n"
               rule._inspect(str, level+2, done)
            end
         end if @sequence
         @mapping.each do |key, rule|
            if done[rule.__id__]
               str << '  ' * (level+1) << '"' << key << "\": ...\n"
            else
               str << '  ' * (level+1) << '"' << key << "\":\n"
               rule._inspect(str, level+2, done)
            end
         end if @mapping
         return str
      end

   end

end
#--end of require 'kwalify/rule'
#--begin of require 'kwalify/validator'
###
### $Rev: 48 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

#--already included require 'kwalify/messages'
#--already included require 'kwalify/errors'
#--already included require 'kwalify/types'
#--already included require 'kwalify/rule'

module Kwalify

   ##
   ## ex.
   ##   schema = YAML.load_file('schema.yaml')
   ##   validator = Kwalify::Validator.new(schema)
   ##   document = YAML.load_file('document.yaml')
   ##   error_list = validator.validate(document)
   ##   unless error_list.empty?
   ##     error_list.each do |error|
   ##       puts "- [#{error.path}] #{error.message}"
   ##     end
   ##   end
   ##
   class Validator
      include Kwalify::ErrorHelper

      def initialize(hash, &block)
         @rule  = Rule.new(hash)
         @block = block
      end
      attr_reader :rule


      def _inspect
         @rule._inspect
      end


      def validate(value)
         path = "";  errors = [];  done = {}
         _validate(value, @rule, path, errors, done)
         return errors
      end


      protected


      def validate_hook(value, rule, path, errors)
      end


      def _validate(value, rule, path, errors, done)
         if Types.collection?(value)
            return if done[value.__id__]     # avoid infinite loop
            done[value.__id__] = true
         end
         if rule.required && value == nil
            #* key=:required_novalue  msg="value required but none."
            errors << validate_error(:required_novalue, rule, path, value)
            return
         end
         if rule.type_class && value != nil && !value.is_a?(rule.type_class)
            #* key=:type_unmatch  msg="not a %s."
            errors << validate_error(:type_unmatch, rule, path, value, [Kwalify.word(rule.type)])
            return
         end
         #
         n = errors.length
         if rule.sequence
            _validate_sequence(value, rule, path, errors, done)
         elsif rule.mapping
            _validate_mapping(value, rule, path, errors, done)
         else
            _validate_scalar(value, rule, path, errors, done)
         end
         return unless errors.length == n
         #
         validate_hook(value, rule, path, errors)
         @block.call(value, rule, path, errors) if @block
      end


      private


      def _validate_scalar(value, rule, path, errors, done)
         assert_error("rule.sequence.class==#{rule.sequence.class.name} (expected NilClass)") if rule.sequence
         assert_error("rule.mapping.class==#{rule.mapping.class.name} (expected NilClass)") if rule.mapping
         if rule.assert_proc
            unless rule.assert_proc.call(value)
               #* key=:assert_failed  msg="assertion expression failed (%s)."
               errors << validate_error(:assert_failed, rule, path, value, [rule.assert])
            end
         end
         if rule.enum
            unless rule.enum.include?(value)
               keyname = File.basename(path)
               keyname = 'enum' if keyname =~ /\A\d+\z/
               #* key=:enum_notexist  msg="invalid %s value."
               errors << validate_error(:enum_notexist, rule, path, value, [keyname])
            end
         end
         #
         return if value == nil
         #
         if rule.pattern
            unless value.to_s =~ rule.regexp
               #* key=:pattern_unmatch  msg="not matched to pattern %s."
               errors << validate_error(:pattern_unmatch, rule, path, value, [rule.pattern])
            end
         end
         if rule.range
            assert_error("value.class=#{value.class.name}") unless Types.scalar?(value)
            if rule.range['max'] && rule.range['max'] < value
               #* key=:range_toolarge  msg="too large (> max %s)."
               errors << validate_error(:range_toolarge, rule, path, value, [rule.range['max'].to_s])
            end
            if rule.range['min'] && rule.range['min'] > value
               #* key=:range_toosmall  msg="too small (< min %s)."
               errors << validate_error(:range_toosmall, rule, path, value, [rule.range['min'].to_s])
            end
            if rule.range['max-ex'] && rule.range['max-ex'] <= value
               #* key=:range_toolargeex  msg="too large (>= max %s)."
               errors << validate_error(:range_toolargeex, rule, path, value, [rule.range['max-ex'].to_s])
            end
            if rule.range['min-ex'] && rule.range['min-ex'] >= value
               #* key=:range_toosmallex  msg="too small (<= min %s)."
               errors << validate_error(:range_toosmallex, rule, path, value, [rule.range['min-ex'].to_s])
            end
         end
         if rule.length
            assert_error("value.class=#{value.class.name}") unless value.is_a?(String) || value.is_a?(Text)
            len = value.to_s.length
            if rule.length['max'] && rule.length['max'] < len
               #* key=:length_toolong  msg="too long (length %d > max %d)."
               errors << validate_error(:length_toolong, rule, path, value, [len, rule.length['max']])
            end
            if rule.length['min'] && rule.length['min'] > len
               #* key=:length_tooshort  msg="too short (length %d < min %d)."
               errors << validate_error(:length_tooshort, rule, path, value, [len, rule.length['min']])
            end
            if rule.length['max-ex'] && rule.length['max-ex'] <= len
               #* key=:length_toolongex  msg="too long (length %d >= max %d)."
               errors << validate_error(:length_toolongex, rule, path, value, [len, rule.length['max-ex']])
            end
            if rule.length['min-ex'] && rule.length['min-ex'] >= len
               #* key=:length_tooshortex  msg="too short (length %d <= min %d)."
               errors << validate_error(:length_tooshortex, rule, path, value, [len, rule.length['min-ex']])
            end
         end
      end


      def _validate_sequence(list, seq_rule, path, errors, done)
         assert_error("seq_rule.sequence.class==#{seq_rule.sequence.class.name} (expected Array)") unless seq_rule.sequence.is_a?(Array)
         assert_error("seq_rule.sequence.length==#{seq_rule.sequence.length} (expected 1)") unless seq_rule.sequence.length == 1
         return if list == nil
         rule = seq_rule.sequence[0]
         list.each_with_index do |val, i|
            _validate(val, rule, "#{path}/#{i}", errors, done)   ## validate recursively
         end
         if rule.type == 'map'
            unique_keys = []
            rule.mapping.keys.each do |key|
               map_rule = rule.mapping[key]
               unique_keys << key if map_rule.unique || map_rule.ident
            end
            unique_keys.each do |key|
               table = {}
               list.each_with_index do |map, i|
                  val = map[key]
                  next if val == nil
                  curr_path = "#{path}/#{i}/#{key}"
                  if table[val]
                     #* key=:value_notunique  msg="is already used at '%s'."
                     errors << validate_error(:value_notunique, rule, "#{path}/#{i}/#{key}", val, "#{path}/#{table[val]}/#{key}")
                  else
                     table[val] = i
                  end
               end
            end if !unique_keys.empty?
         elsif rule.unique
            table = {}
            list.each_with_index do |val, i|
               next if val == nil
               if table[val]
                  #  #* key=:value_notunique  msg="is already used at '%s'."
                  errors << validate_error(:value_notunique, rule, "#{path}/#{i}", val, "#{path}/#{table[val]}")
               else
                  table[val] = i
               end
            end
         end
      end


      def _validate_mapping(hash, map_rule, path, errors, done)
         assert_error("map_rule.mapping.class==#{map_rule.mapping.class.name} (expected Hash)") unless map_rule.mapping.is_a?(Hash)
         return if hash == nil
         map_rule.mapping.each do |key, rule|
            if rule.required && !hash.key?(key)
               #* key=:required_nokey  msg="key '%s:' is required."
               errors << validate_error(:required_nokey, rule, path, hash, [key])
            end
         end
         hash.each do |key, val|
            rule = map_rule.mapping[key]
            unless rule
               #* key=:key_undefined  msg="key '%s' is undefined."
               errors << validate_error(:key_undefined, rule, "#{path}/#{key}", hash, ["#{key}:"])
               ##* key=:key_undefined  msg="undefined key."
               #errors << validate_error(:key_undefined, rule, "#{path}/#{key}", "#{key}:")
            else
               _validate(val, rule, "#{path}/#{key}", errors, done)   ## validate recursively
            end
         end
      end

   end

end
#--end of require 'kwalify/validator'
#--begin of require 'kwalify/meta-validator'
###
### $Rev: 51 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

#--already included require 'kwalify/errors'
#--already included require 'kwalify/rule'
#--already included require 'kwalify/validator'
require 'yaml'

module Kwalify


   ##
   ## ex.
   ##   meta_validator = Kwalify::MetaValidator.instance()
   ##   schema = File.load_file('schema.yaml')
   ##   errors = meta_validator.validate(schema)
   ##   if !errors.empty?
   ##     errors.each do |error|
   ##       puts "[#{error.path}] #{error.message}"
   ##     end
   ##   end
   ##
   class MetaValidator < Validator

      META_SCHEMA = <<'END'
name:      MAIN
type:      map
required:  yes
mapping:   &main-rule
 "name":
    type:      str
 "desc":
    type:      str
 "classname":
    type:      str
 "type":
    type:      str
    #required:  yes
    enum:
      - seq
      #- sequence
      #- list
      - map
      #- mapping
      #- hash
      - str
      #- string
      - int
      #- integer
      - float
      - number
      #- numeric
      - bool
      #- boolean
      - text
      - date
      - time
      - timestamp
      #- object
      - any
      - scalar
      #- collection
 "required":
    type:      bool
 "enum":
    type:      seq
    sequence:
      - type:     scalar
        unique:   yes
 "pattern":
    type:      str
 "assert":
    type:      str
    pattern:   /\bval\b/
 "range":
    type:      map
    mapping:
     "max":
        type:     scalar
     "min":
        type:     scalar
     "max-ex":
        type:     scalar
     "min-ex":
        type:     scalar
 "length":
    type:      map
    mapping:
     "max":
        type:     int
     "min":
        type:     int
     "max-ex":
        type:     int
     "min-ex":
        type:     int
 "ident":
    type:      bool
 "unique":
    type:      bool
 "sequence":
    name:      SEQUENCE
    type:      seq
    sequence:
      - type:      map
        mapping:   *main-rule
        name:      MAIN
        #required:  yes
 "mapping":
    name:      MAPPING
    type:      map
    mapping:
      =:
        type:      map
        mapping:   *main-rule
        name:      MAIN
        #required:  yes
END


      def initialize(schema, &block)
         super
      end

      def validate_hook(value, rule, path, errors)
         return if value == nil     ## realy?
         return unless rule.name == "MAIN"
         #
         hash = value
         type = hash['type']
         type = Types::DEFAULT_TYPE if type == nil
         klass = Types.type_class(type)
         #unless klass
         #   errors << validate_error(:type_unknown, rule, "#{path}/type", type)
         #end
         #
         if hash.key?('classname')
            val = hash['classname']
            unless val.nil? || type == 'map'
               errors << validate_error(:classname_notmap, rule, "#{path}/classname", 'classname:')
            end
         end
         #
         if hash.key?('pattern')
            val = hash['pattern']
            pat = (val =~ /\A\/(.*)\/([mi]?[mi]?)\z/ ? $1 : val)
            begin
               Regexp.compile(pat)
            rescue RegexpError => ex
               errors << validate_error(:pattern_syntaxerr, rule, "#{path}/pattern", val)
            end
         end
         #
         if hash.key?('enum')
            if Types.collection_type?(type)
               errors << validate_error(:enum_notscalar, rule, path, 'enum:')
            else
               #elem_table = {}
               hash['enum'].each do |elem|
                  #if elem_table[elem]
                  #   errors << validate_error(:enum_duplicate, rule, "#{path}/enum", elem.to_s)
                  #end
                  #elem_table[elem] = true
                  unless elem.is_a?(klass)
                     errors << validate_error(:enum_type_unmatch, rule, "#{path}/enum", elem, [Kwalify.word(type)])
                  end
               end
            end
         end
         #
         if hash.key?('assert')
            val =  hash['assert']
            #val =~ /\bval\b/ or errors << validate_error(:assert_noval, rule, "#{path}/assert", val)
            begin
               eval "proc { |val| #{val} }"
            rescue SyntaxError => ex
               errors << validate_error(:assert_syntaxerr, rule, "#{path}/assert", val)
            end
         end
         #
         if hash.key?('range')
            val = hash['range']
            curr_path = path + "/range"
            #if ! val.is_a?(Hash)
            #   errors << validate_error(:range_notmap, rule, curr_path, val)
            #elsif ...
            if Types.collection_type?(type) || type == 'bool' || type == 'any'
               errors << validate_error(:range_notscalar, rule, path, 'range:')
            else
               val.each do |rkey, rval|
                  #case rkey
                  #when 'max', 'min', 'max-ex', 'min-ex'
                     unless rval.is_a?(klass)
                        typename = Kwalify.word(type) || type
                        errors << validate_error(:range_type_unmatch, rule, "#{curr_path}/#{rkey}", rval, [typename])
                     end
                  #else
                  #   errors << validate_error(:range_undefined, rule, curr_path, "#{rkey}:")
                  #end
               end
            end
            if val.key?('max') && val.key?('max-ex')
               errors << validate_error(:range_twomax, rule, curr_path, nil)
            end
            if val.key?('min') && val.key?('min-ex')
               errors << validate_error(:range_twomin, rule, curr_path, nil)
            end
            max, min, max_ex, min_ex = val['max'], val['min'], val['max-ex'], val['min-ex']
            if max
               if min && max < min
                  errors << validate_error(:range_maxltmin, rule, curr_path, nil, [max, min])
               elsif min_ex && max <= min_ex
                  errors << validate_error(:range_maxleminex, rule, curr_path, nil, [max, min_ex])
               end
            elsif max_ex
               if min && max_ex <= min
                  errors << validate_error(:range_maxexlemin, rule, curr_path, nil, [max_ex, min])
               elsif min_ex && max_ex <= min_ex
                  errors << validate_error(:range_maxexleminex, rule, curr_path, nil, [max_ex, min_ex])
               end
            end
         end
         #
         if hash.key?('length')
            val = hash['length']
            curr_path = path + "/length"
            #val.is_a?(Hash) or errors << validate_error(:length_notmap, rule, curr_path, val)
            unless type == 'str' || type == 'text'
               errors << validate_error(:length_nottext, rule, path, 'length:')
            end
            #val.each do |lkey, lval|
            #   #case lkey
            #   #when 'max', 'min', 'max-ex', 'min-ex'
            #      unless lval.is_a?(Integer)
            #         errors << validate_error(:length_notint, rule, "#{curr_path}/#{lkey}", lval)
            #      end
            #   #else
            #   #   errors << validate_error(:length_undefined, rule, curr_path, "#{lkey}:")
            #   #end
            #end
            if val.key?('max') && val.key?('max-ex')
               errors << validate_error(:length_twomax, rule, curr_path, nil)
            end
            if val.key?('min') && val.key?('min-ex')
               errors << validate_error(:length_twomin, rule, curr_path, nil)
            end
            max, min, max_ex, min_ex = val['max'], val['min'], val['max-ex'], val['min-ex']
            if max
               if min && max < min
                  errors << validate_error(:length_maxltmin, rule, curr_path, nil, [max, min])
               elsif min_ex && max <= min_ex
                  errors << validate_error(:length_maxleminex, rule, curr_path, nil, [max, min_ex])
               end
            elsif max_ex
               if min && max_ex <= min
                  errors << validate_error(:length_maxexlemin, rule, curr_path, nil, [max_ex, min])
               elsif min_ex && max_ex <= min_ex
                  errors << validate_error(:length_maxexleminex, rule, curr_path, nil, [max_ex, min_ex])
               end
            end
         end
         #
         if hash.key?('unique')
            if hash['unique'] && Types.collection_type?(type)
               errors << validate_error(:unique_notscalar, rule, path, "unique:")
            end
            if path.empty?
               errors << validate_error(:unique_onroot, rule, "/", "unique:")
            end
         end
         #
         if hash.key?('ident')
            if hash['ident'] && Types.collection_type?(type)
               errors << validate_error(:ident_notscalar, rule, path, "ident:")
            end
            if path.empty?
               errors << validate_error(:ident_onroot, rule, "/", "ident:")
            end
         end
         #
         if hash.key?('sequence')
            val = hash['sequence']
            #if val != nil && !val.is_a?(Array)
            #   errors << validate_error(:sequence_notseq,  rule, "#{path}/sequence", val)
            #elsif ...
            if val == nil || val.empty?
               errors << validate_error(:sequence_noelem,  rule, "#{path}/sequence", val)
            elsif val.length > 1
               errors << validate_error(:sequence_toomany, rule, "#{path}/sequence", val)
            else
               elem = val[0]
               assert_error("elem.class=#{elem.class}") unless elem.is_a?(Hash)
               if elem['ident'] && elem['type'] != 'map'
                  errors << validate_error(:ident_notmap, nil, "#{path}/sequence/0", 'ident:')
               end
            end
         end
         #
         if hash.key?('mapping')
            val = hash['mapping']
            if val != nil && !val.is_a?(Hash)
               errors << validate_error(:mapping_notmap, rule, "#{path}/mapping", val)
            elsif val == nil || (val.empty? && !val.default)
               errors << validate_error(:mapping_noelem, rule, "#{path}/mapping", val)
            end
         end
         #
         if type == 'seq'
            errors << validate_error(:seq_nosequence, rule, path, nil)      unless hash.key?('sequence')
            #errors << validate_error(:seq_conflict, rule, path, 'enum:')        if hash.key?('enum')
            errors << validate_error(:seq_conflict, rule, path, 'pattern:')     if hash.key?('pattern')
            errors << validate_error(:seq_conflict, rule, path, 'mapping:')     if hash.key?('mapping')
            #errors << validate_error(:seq_conflict, rule, path, 'range:')       if hash.key?('range')
            #errors << validate_error(:seq_conflict, rule, path, 'length:')      if hash.key?('length')
         elsif type == 'map'
            errors << validate_error(:map_nomapping, rule, path, nil)       unless hash.key?('mapping')
            #errors << validate_error(:map_conflict, rule, path, 'enum:')        if hash.key?('enum')
            errors << validate_error(:map_conflict, rule, path, 'pattern:')     if hash.key?('pattern')
            errors << validate_error(:map_conflict, rule, path, 'sequence:')    if hash.key?('sequence')
            #errors << validate_error(:map_conflict, rule, path, 'range:')       if hash.key?('range')
            #errors << validate_error(:map_conflict, rule, path, 'length:')      if hash.key?('length')
         else
            errors << validate_error(:scalar_conflict, rule, path, 'sequence:') if hash.key?('sequence')
            errors << validate_error(:scalar_conflict, rule, path, 'mapping:')  if hash.key?('mapping')
            if hash.key?('enum')
               errors << validate_error(:enum_conflict, rule, path, 'range:')   if hash.key?('range')
               errors << validate_error(:enum_conflict, rule, path, 'length:')  if hash.key?('length')
               errors << validate_error(:enum_conflict, rule, path, 'pattern:') if hash.key?('pattern')
            end
         end

      end  # end of def validate_hook()


      schema = YAML.load(META_SCHEMA)
      @instance = MetaValidator.new(schema)

      def self.instance()
         return @instance
      end


   end # end of class MetaValidator


   META_VALIDATOR = MetaValidator.instance()

   def self.meta_validator           # obsolete
      return META_VALIDATOR
   end

end
#--end of require 'kwalify/meta-validator'
#--begin of require 'kwalify/yaml-parser'
###
### $Rev: 51 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

#--already included require 'kwalify/messages'
#--already included require 'kwalify/errors'
#--already included require 'kwalify/types'

require 'date'


module Kwalify

   ##
   ## ex.
   ##   str = ARGF.read()
   ##   parser = Kwalify::PlainYamlParser.new(str)
   ##   doc = parser.parse()
   ##   p doc
   ##
   class PlainYamlParser

      class Alias
         def initialize(label, linenum)
            @label   = label
            @linenum = linenum
         end
         attr_reader :label, :linenum
      end


      def initialize(yaml_str)
         @lines = yaml_str.to_a()
         @line  = nil
         @linenum = 0
         @anchors = {}
         @aliases = {}
      end


      def parse()
         data = parse_child(0)
         if data == nil && @end_flag == '---'
            data = parse_child(0)
         end
         resolve_aliases(data) unless @aliases.empty?
         return data
      end


      def has_next?
         return @end_flag != 'EOF'
      end


      def parse_all
         list = []
         while has_next()
            doc = parse()
            list << doc
         end
         return list
      end


      protected


      def create_sequence(linenum=nil)
         return []
      end

      def add_to_seq(seq, value, linenum)
         seq << value
      end

      def set_seq_at(seq, i, value, linenum)
         seq[i] = value
      end

      def create_mapping(linenum=nil)
         return {}
      end

      def add_to_map(map, key, value, linenum)
         map[key] = value
      end

      def set_map_with(map, key, value, linenum)
         map[key] = value
      end

      def set_default(map, value, linenum)
         map.value = value
      end

      def merge_map(map, map2, linenum)
         map2.each do |key, val|
            map[key] = value unless map.key?(key)
         end
      end

      def create_scalar(value, linenum=nil)
         return value
      end


      def current_line
         return @line
      end

      def current_linenum
         return @linenum
      end


      private


      def getline
         line = _getline()
         line = _getline() while line && line =~ /^\s*($|\#)/
         return line
      end

      def _getline
         @line = @lines[@linenum]
         @linenum += 1
         case @line
         when nil             ; @end_flag = 'EOF'
         when /^\.\.\.$/      ; @end_flag = '...'; @line = nil
         when /^---(\s+.*)?$/ ; @end_flag = '---'; @line = nil
         else                 ; @end_flag = nil
         end
         return @line
      end


      def reset_sbuf(str)
         @sbuf = str[-1] == ?\n ? str : str + "\n"
         @index = -1
      end


      def _getchar
         @index += 1
         ch = @sbuf[@index]
         while ch == nil
            break if (line = getline()) == nil
            reset_sbuf(line)
            @index += 1
            ch = @sbuf[@index]
         end
         return ch
      end

      def getchar
         ch = _getchar()
         ch = _getchar() while ch && white?(ch)
         return ch
      end

      def getchar_or_nl
         ch = _getchar()
         ch = _getchar() while ch && white?(ch) && ch != ?\n
         return ch
      end

      def current_char
         return @sbuf[@index]
      end


      def syntax_error(error_symbol, linenum=@linenum)
         msg = Kwalify.msg(error_symbol) % [linenum]
         return Kwalify::YamlSyntaxError.new(msg, linenum,error_symbol)
      end

      def parse_child(column)
         line = getline()
         return create_scalar(nil) if !line
         line =~ /^( *)(.*)/
         indent = $1.length
         return create_scalar(nil) if indent < column
         value = $2
         return parse_value(column, value, indent)
      end


      def parse_value(column, value, value_start_column)
         case value
         when /^-( |$)/
            data = parse_sequence(value_start_column, value)
         when /^(:?:?[-.\w]+\*?|'.*?'|".*?"|=|<<) *:( |$)/
         #when /^:?["']?[-.\w]+["']? *:( |$)/			#'
            data = parse_mapping(value_start_column, value)
         when /^\[/, /^\{/
            data = parse_flowstyle(column, value)
         when /^\&[-\w]+( |$)/
            data = parse_anchor(column, value)
         when /^\*[-\w]+( |$)/
            data = parse_alias(column, value)
         when /^[|>]/
            data = parse_block_text(column, value)
         when /^!/
            data = parse_tag(column, value)
         when /^\#/
            data = parse_child(column)
         else
            data = parse_scalar(column, value)
         end
         return data
      end

      def white?(ch)
         return ch == ?\  || ch == ?\t || ch == ?\n || ch == ?\r
      end


      ##
      ## flowstyle     ::=  flow_seq | flow_map | flow_scalar
      ##
      ## flow_seq      ::=  '[' [ flow_seq_item { ',' sp flow_seq_item } ] ']'
      ## flow_seq_item ::=  flowstyle
      ##
      ## flow_map      ::=  '{' [ flow_map_item { ',' sp flow_map_item } ] '}'
      ## flow_map_item ::=  flowstyle ':' sp flowstyle
      ##
      ## flow_scalar   ::=  string | number | boolean | symbol | date
      ##

      def parse_flowstyle(column, value)
         reset_sbuf(value)
         getchar()
         data = parse_flow(0)
         ch = current_char
         assert ch == ?] || ch == ?}
         ch = getchar_or_nl()
         unless ch == ?\n || ch == ?# || ch == nil
            #* key=:flow_hastail  msg="flow style sequence is closed but got '%s'."
            raise syntax_error(:flow_hastail, [ch.chr])
         end
         getline() if ch != nil
         return data
      end

      def parse_flow(depth)
         ch = current_char()
         #ch = getchar()
         if ch == nil
            #* key=:flow_eof  msg="found EOF when parsing flow style."
            rase syntax_error(:flow_eof)
         end
         if ch == ?[
            data = parse_flow_seq(depth)
         elsif ch == ?{
            data = parse_flow_map(depth)
         else
            data = parse_flow_scalar(depth)
         end
         return data
      end

      def parse_flow_seq(depth)
         assert current_char() == ?[
         seq = create_sequence()  # []
         ch = getchar()
         if ch != ?}
            linenum = current_linenum()
            #seq << parse_flow_seq_item(depth + 1)
            add_to_seq(seq, parse_flow_seq_item(depth + 1), linenum)
            while (ch = current_char()) == ?,
               ch = getchar()
               if ch == ?]
                  #* key=:flow_noseqitem  msg="sequence item required (or last comma is extra)."
                  raise syntax_error(:flow_noseqitem)
               end
               #break if ch == ?]
               linenum = current_linenum()
               #seq << parse_flow_seq_item(depth + 1)
               add_to_seq(seq, parse_flow_seq_item(depth + 1), linenum)
            end
         end
         unless current_char() == ?]
            #* key=:flow_seqnotclosed  msg="flow style sequence requires ']'."
            raise syntax_error(:flow_seqnotclosed)
         end
         getchar() if depth > 0
         return seq
      end

      def parse_flow_seq_item(depth)
         return parse_flow(depth)
      end

      def parse_flow_map(depth)
         assert current_char() == ?{          #}
         map = create_mapping()  # {}
         ch = getchar()
         if ch != ?}
            linenum = current_linenum()
            key, value = parse_flow_map_item(depth + 1)
            #map[key] = value
            add_to_map(map, key, value, linenum)
            while (ch = current_char()) == ?,
               ch = getchar()
               if ch == ?}
                  #* key=:flow_mapnoitem  msg="mapping item required (or last comma is extra)."
                  raise syntax_error(:flow_mapnoitem)
               end
               #break if ch == ?}
               linenum = current_linenum()
               key, value = parse_flow_map_item(depth + 1)
               #map[key] = value
               add_to_map(map, key, value, linenum)
            end
         end
         unless current_char() == ?}
            #* key=:flow_mapnotclosed  msg="flow style mapping requires '}'."
            raise syntax_error(:flow_mapnotclosed)
         end
         getchar() if depth > 0
         return map
      end

      def parse_flow_map_item(depth)
         key = parse_flow(depth)
         unless (ch = current_char()) == ?:
            s = ch ? "'#{ch.chr}'" : "EOF"
            #* key=:flow_nocolon  msg="':' expected but got '%s'."
            raise syntax_error(:flow_nocolon)
         end
         getchar()
         value = parse_flow(depth)
         return key, value
      end

      def parse_flow_scalar(depth)
         case ch = current_char()
         when ?", ?'         #"
            endch = ch
            s = ''
            while (ch = _getchar()) != nil && ch != endch
               s << ch.chr
            end
            getchar()
            scalar = s
         else
            s = ch.chr
            while (ch = _getchar()) != nil && ch != ?: && ch != ?, && ch != ?] && ch != ?}
               s << ch.chr
            end
            scalar = to_scalar(s.strip)
         end
         return create_scalar(scalar)
      end


      def parse_tag(column, value)
         assert value =~ /^!\S+/
         value =~ /^!(\S+)((\s+)(.*))?$/
         tag    = $1
         space  = $3
         value2 = $4
         if value2 && !value2.empty?
            value_start_column = column + 1 + tag.length + space.length
            data = parse_value(column, value2, value_start_column)
         else
            data = parse_child(column)
         end
         return data
      end


      def parse_anchor(column, value)
         assert value =~ /^\&([-\w]+)(( *)(.*))?$/
         label  = $1
         space  = $3
         value2 = $4
         if value2 && !value2.empty?
            #column2 = column + 1 + label.length + space.length
            #data = parse_value(column2, value2)
            value_start_column = column + 1 + label.length + space.length
            data = parse_value(column, value2, value_start_column)
         else
            #column2 = column + 1
            #data = parse_child(column2)
            data = parse_child(column)
         end
         register_anchor(label, data)
         return data
      end

      def register_anchor(label, data)
         if @anchors[label]
            #* key=:anchor_duplicated  msg="anchor '%s' is already used."
            raise syntax_error(:anchor_duplicated, [label])
         end
         @anchors[label] = data
      end

      def parse_alias(column, value)
         assert value =~ /^\*([-\w]+)(( *)(.*))?$/
         label  = $1
         space  = $3
         value2 = $4
         if value2 && !value2.empty? && value2[0] != ?\#
            #* key=:alias_extradata  msg="alias cannot take any data."
            raise syntax_error(:alias_extradata)
         end
         data = @anchors[label]
         unless data
            data = register_alias(label)
            #raise syntax_error("anchor '#{label}' not found (cannot refer to backward or child anchor).")
         end
         getline()
         return data
      end

      def register_alias(label)
         @aliases[label] ||= 0
         @aliases[label] += 1
         return Alias.new(label, @linenum)
      end


      def resolve_aliases(data)
         @resolved ||= {}
         return if @resolved[data.__id__]
         @resolved[data.__id__] = data
         case data
         when Array
            seq = data
            seq.each_with_index do |val, i|
               if val.is_a?(Alias)
                  anchor = val
                  if @anchors.key?(anchor.label)
                     #seq[i] = @anchors[anchor.label]
                     set_seq_at(seq, i, @anchors[anchor.label], anchor.linenum)
                  else
                     #* key=:anchor_notfound  msg="anchor '%s' not found"
                     raise syntax_error(:anchor_notfound, [val.linenum])
                  end
               elsif val.is_a?(Array) || val.is_a?(Hash)
                  resolve_aliases(val)
               end
            end
         when Hash
            map = data
            map.each do |key, val|
               if val.is_a?(Alias)
                  if @anchors.key?(val.label)
                     anchor = val
                     #map[key] = @anchors[anchor.label]
                     set_map_with(map, key, @anchors[anchor.label], anchor.linenum)
                  else
                     ## :anchor_notfound is already defined on above
                     raise syntax_error(:anchor_notfound, [val.linenum])
                  end
               elsif val.is_a?(Array) || val.is_a?(Hash)
                  resolve_aliases(val)
               end
            end
         else
            assert !data.is_a?(Alias)
         end
      end


      def parse_block_text(column, value)
         assert value =~ /^[>|\|]/
         value =~ /^([>|\|])([-+]?)(\d+)?\s*(.*)$/
         char = $1
         indicator = $2
         sep = char == "|" ? "\n" : " "
         margin = $3 && !$3.empty? ? $3.to_i : nil
         #text = $4.empty? ? '' :  $4 + sep
         text = $4
         s = ''
         empty = ''
         min_indent = -1
         while line = _getline()
            line =~ /^( *)(.*)/
            indent = $1.length
            if $2.empty?
               empty << "\n"
            elsif indent < column
               break
            else
               min_indent = indent if min_indent < 0 || min_indent > indent
               s << empty << line
               empty = ''
            end
         end
         s << empty if indicator == '+' && char != '>'
         s[-1] = "" if indicator == '-'
         min_indent = column + margin - 1 if margin
         if min_indent > 0
            sp = ' ' * min_indent
            s.gsub!(/^#{sp}/, '')
         end
         if char == '>'
            s.gsub!(/([^\n])\n([^\n])/, '\1 \2')
            s.gsub!(/\n(\n+)/, '\1')
            s << empty if indicator == '+'
         end
         getline() if current_line() =~ /^\s*\#/
         return create_scalar(text + s)
      end


      def parse_sequence(column, value)
         assert value =~ /^-(( +)(.*))?$/
         seq = create_sequence()  # []
         while true
            unless value =~ /^-(( +)(.*))?$/
               #* key=:sequence_noitem  msg="sequence item is expected."
               raise syntax_error(:sequence_noitem)
            end
            value2 = $3
            space  = $2
            column2 = column + 1
            linenum = current_linenum()
            #
            if !value2 || value2.empty?
               elem = parse_child(column2)
            else
               value_start_column = column2 + space.length
               elem = parse_value(column2, value2, value_start_column)
            end
            add_to_seq(seq, elem, linenum)    #seq << elem
            #
            line = current_line()
            break unless line
            line =~ /^( *)(.*)/
            indent = $1.length
            if    indent < column
               break
            elsif indent > column
               #* key=:sequence_badindent  msg="illegal indent of sequence."
               raise syntax_error(:sequence_badindent)
            end
            value = $2
         end
         return seq
      end


      def parse_mapping(column, value)
         #assert value =~ /^(:?["']?[-.\w]+["']? *):(( +)(.*))?$/         #'
         assert value =~ /^((?::?[-.\w]+\*?|'.*?'|".*?"|=|<<) *):(( +)(.*))?$/
         map = create_mapping()  # {}
         while true
            #unless value =~ /^(:?["']?[-.\w]+["']? *):(( +)(.*))?$/      #'
            unless value =~ /^((?::?[-.\w]+\*?|'.*?'|".*?"|=|<<) *):(( +)(.*))?$/
               #* key=:mapping_noitem  msg="mapping item is expected."
               raise syntax_error(:mapping_noitem)
            end
            v = $1.strip
            key = to_scalar(v)
            value2 = $4
            column2 = column + 1
            linenum = current_linenum()
            #
            if !value2 || value2.empty?
               elem = parse_child(column2)
            else
               value_start_column = column2 + $1.length + $3.length
               elem = parse_value(column2, value2, value_start_column)
            end
            case v
            when '='
               set_default(map, elem, linenum)
            when '<<'
               merge_map(map, elem, linenum)
            else
               add_to_map(map, key, elem, linenum)    # map[key] = elem
            end
            #
            line = current_line()
            break unless line
            line =~ /^( *)(.*)/
            indent = $1.length
            if    indent < column
               break
            elsif indent > column
               #* key=:mapping_badindent  msg="illegal indent of mapping."
               raise syntax_error(:mapping_badindent)
            end
            value = $2
         end
         return map
      end


      def parse_scalar(indent, value)
         data = create_scalar(to_scalar(value))
         getline()
         return data
      end


      def to_scalar(str)
         case str
         when /^"(.*)"([ \t]*\#.*$)?/    ; return $1
         when /^'(.*)'([ \t]*\#.*$)?/    ; return $1
         when /^(.*\S)[ \t]*\#/          ; str = $1
         end

         case str
         when /^-?\d+$/              ;  return str.to_i    # integer
         when /^-?\d+\.\d+$/         ;  return str.to_f    # float
         when "true", "yes", "on"    ;  return true        # true
         when "false", "no", "off"   ;  return false       # false
         when "null", "~"            ;  return nil         # nil
         #when /^"(.*)"$/             ;  return $1          # "string"
         #when /^'(.*)'$/             ;  return $1          # 'string'
         when /^:(\w+)$/             ;  return $1.intern   # :symbol
         when /^(\d\d\d\d)-(\d\d)-(\d\d)$/                 # date
            year, month, day = $1.to_i, $2.to_i, $3.to_i
            return Date.new(year, month, day)
         when /^(\d\d\d\d)-(\d\d)-(\d\d)(?:[Tt]|[ \t]+)(\d\d?):(\d\d):(\d\d)(\.\d*)?(?:Z|[ \t]*([-+]\d\d?)(?::(\d\d))?)?$/
            year, mon, mday, hour, min, sec, usec, tzone_h, tzone_m = $1, $2, $3, $4, $5, $6, $7, $8, $9
            #Time.utc(sec, min, hour, mday, mon, year, wday, yday, isdst, zone)
            #t = Time.utc(sec, min, hour, mday, mon, year, nil, nil, nil, nil)
            #Time.utc(year[, mon[, day[, hour[, min[, sec[, usec]]]]]])
            time = Time.utc(year, mon, day, hour, min, sec, usec)
            if tzone_h
               diff_sec = tzone_h.to_i * 60 * 60
               if tzone_m
                  if diff_sec > 0 ; diff_sec += tzone_m.to_i * 60
                  else            ; diff_sec -= tzone_m.to_i * 60
                  end
               end
               p diff_sec
               time -= diff_sec
            end
            return time
         end
         return str
      end


      def assert(bool_expr)
         raise "*** assertion error" unless bool_expr
      end

   end



   ##
   ## ex.
   ##  # load document with YamlParser
   ##  str = ARGF.read()
   ##  parser = Kwalify::YamlParser.new(str)
   ##  document = parser.parse()
   ##
   ##  # validate document
   ##  schema = YAML.load(File.read('schema.yaml'))
   ##  validator = Kwalify::Validator.new(schema)
   ##  errors = validator.validate(document)
   ##
   ##  # print validation result
   ##  if errors && !errors.empty?
   ##    parser.set_errors_linenum(errors)
   ##    errors.sort.each do |error|
   ##      print "line %d: path %s: %s" % [error.linenum, error.path, error.message]
   ##    end
   ##  end
   ##
   class YamlParser < PlainYamlParser

      def initialize(*args)
         super
         @linenums_table = {}     # object_id -> hash or array
      end

      def parse()
         @doc = super()
         return @doc
      end

      def path_linenum(path)
         return 1 if path.empty? || path == '/'
         elems = path.split('/')
         elems.shift if path[0] == ?/    # delete empty string on head
         last_elem = elems.pop
         c = @doc   # collection
         elems.each do |elem|
            if c.is_a?(Array)
               c = c[elem.to_i]
            elsif c.is_a?(Hash)
               c = c[elem]
            else
               assert false
            end
         end
         linenums = @linenums_table[c.__id__]
         if c.is_a?(Array)
            linenum = linenums[last_elem.to_i]
         elsif c.is_a?(Hash)
            linenum = linenums[last_elem]
         end
         return linenum
      end

      def set_errors_linenum(errors)
         errors.each do |error|
            error.linenum = path_linenum(error.path)
         end
      end

      def set_error_linenums(errors)
         $stderr.puts "*** Kwalify::YamlParser#set_error_linenums() is obsolete. You should use set_errors_linenum() instead."
         set_errors_linenum(errors)
      end

      protected

      def create_sequence(linenum=current_linenum())
         seq = []
         @linenums_table[seq.__id__] = []
         return seq
      end

      def add_to_seq(seq, value, linenum)
         seq << value
         @linenums_table[seq.__id__] << linenum
      end

      def set_seq_at(seq, i, value, linenum)
         seq[i] = value
         @linenums_table[seq.__id__][i] = linenum
      end

      def create_mapping(linenum=current_linenum())
         map = {}
         @linenums_table[map.__id__] = {}
         return map
      end

      def add_to_map(map, key, value, linenum)
         map[key] = value
         @linenums_table[map.__id__][key] = linenum
      end

      def set_map_with(map, key, value, linenum)
         map[key] = value
         @linenums_table[map.__id__][key] = linenum
      end

      def set_default(map, value, linenum)
         map.default = value
         @linenums_table[map.__id__][:'='] = linenum
      end

      def merge_map(map, collection, linenum)
         t = @linenums_table[map.__id__]
         list = collection.is_a?(Array) ? collection : [ collection ]
         list.each do |m|
            t2 = @linenums_table[m.__id__]
            m.each do |key, val|
               unless map.key?(key)
                  map[key] = val
                  t[key] = t2[key]
               end
            end
         end
      end

      def create_scalar(value, linenum=current_linenum())
         data = super(value)
         #return Scalar.new(data, linenum)
         return data
      end

   end


   ## obsolete
   class Parser < YamlParser
      def initialize(yaml_str)
         super(yaml_str)
         $stderr.puts "*** class Kwalify::Parser is obsolete. Please use Kwalify::YamlParser instead."
      end
   end

end
#--end of require 'kwalify/yaml-parser'
#--end of require 'kwalify'
#--begin of require 'kwalify/main'
###
### $Rev: 51 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

require 'yaml'
require 'erb'
#--already included require 'kwalify'
#--begin of require 'kwalify/util/yaml-helper'
###
### $Rev: 51 $
### $Release: 0.6.1 $
### copyright(c) 2005 kuwata-lab all rights reserved.
###

require 'yaml'

module YamlHelper

   ##
   ## expand tab character to spaces
   ##
   ## ex.
   ##   untabified_str = YamlHelper.untabify(tabbed_str)
   ##
   ## input:: String or IO
   ##
   def self.untabify(input)
      s = ''
      input.each_line do |line|
         s << line.gsub(/([^\t]{8})|([^\t]*)\t/n) { [$+].pack("A8") }
      end
      return s
   end


   ##
   ## create a hash table from list of hash with primary key.
   ##
   ## ex.
   ##   hashlist = [
   ##     { "name"=>"Foo", "gender"=>"M", "age"=>20, },
   ##     { "name"=>"Bar", "gender"=>"F", "age"=>25, },
   ##     { "name"=>"Baz", "gender"=>"M", "age"=>30, },
   ##   ]
   ##   hashtable = YamlHelper.create_hashtable(hashlist, "name")
   ##   p hashtable
   ##       # => { "Foo" => { "name"=>"Foo", "gender"=>"M", "age"=>20, },
   ##              "Bar" => { "name"=>"Bar", "gender"=>"F", "age"=>25, },
   ##              "Baz" => { "name"=>"Baz", "gender"=>"M", "age"=>30, }, }
   ##
   def self.create_hashtable(hashlist, primarykey, flag_duplicate_check=true)
      hashtable = {}
      hashlist.each do |hash|
         key = hash[primarykey]
         unless key
            riase "primary key '#{key}' not found."
         end
         if flag_duplicate_check && hashtable.key?(key)
            raise "primary key '#{key}' duplicated (value '#{hashtable[key]}')"
         end
         hashtable[key] = hash
      end if hashlist
      return hashtable
   end


   ##
   ## get nested value directly.
   ##
   ## ex.
   ##   val = YamlHelper.get_value(obj, ['aaa', 0, 'xxx'])
   ##
   ## This is equal to the following:
   ##   begin
   ##     val = obj['aaa'][0]['xxx']
   ##   rescue NameError
   ##     val = nil
   ##   end
   ##
   def self.get_value(obj, path)
      val = obj
      path.each do |key|
         return nil unless val.is_a?(Hash) || val.is_a?(Array)
         val = val[key]
      end if path
      return val
   end

end

#--end of require 'kwalify/util/yaml-helper'
#require 'kwalify/util/option-parser'


module Kwalify


   class CommandOptionError < KwalifyError
      def initialize(message, option, error_symbol)
         super(message)
         @option = option
         @error_symbol = error_symbol
      end
      attr_reader :option, :error_symbol
   end


   ##
   ## ex.
   ##   command = File.basename($0)
   ##   begin
   ##      main = Kwalify::Main.new(command)
   ##      s = main.execute
   ##      print s if s
   ##   rescue Kwalify::CommandOptionError => ex
   ##      $stderr.puts "ERROR: #{ex.message}"
   ##      exit 1
   ##   rescue Kwalify::KwalifyError => ex
   ##      $stderr.puts "ERROR: #{ex.message}"
   ##      exit 1
   ##   end
   ##
   class Main


      def initialize(command=nil)
         @command = command || File.basename($0)
         @options = {}
         @properties    = {}
         @template_path  = []
         $:.each do |path|
            tpath = "#{path}/kwalify/templates"
            @template_path << tpath if test(?d, tpath)
         end
      end


      def debug?
         @options[:debug]
      end


      def _inspect()
         sb = []
         sb <<    "command: #{@command}\n"
         sb <<    "options:\n"
         @options.keys.sort {|k1,k2| k1.to_s<=>k2.to_s }.each do |key|
            sb << "  - #{key}: #{@options[key]}\n"
         end
         sb <<    "properties:\n"
         @properties.keys.sort_by {|k| k.to_s}.each do |key|
            sb << "  - #{key}: #{@properties[key]}\n"
         end
         #sb <<    "template_path:\n"
         #@template_path.each do |path|
         #   sb << "  - #{path}\n"
         #end
         return sb.join
      end


      def execute(argv=ARGV)
         # parse command-line options
         filenames = _parse_argv(argv)

         # help or version
         if @options[:help] || @options[:version]
            action = @options[:action]
            s = ''
            s << _version() << "\n"           if @options[:version]
            s << _usage()                     if @options[:help] && !action
            s << _describe_properties(action) if @options[:help] && action
            return s
         end

         # validation
         if @options[:meta2]
            s = _quick_meta_validate(filenames)
         elsif @options[:meta]
            s = _meta_validate(filenames)
         elsif @options[:action]
            if !@options[:schema]
               #* key=:command_option_actionnoschema  msg="schema filename is not specified."
               raise option_error(:command_option_actionnoschema, @options[:action])
            end
            s = _perform_action(@options[:action], @options[:schema])
         elsif @options[:schema]
            if @options[:debug]
               s = _inspect_schema(@options[:schema])
            else
               s = _validate(filenames, @options[:schema])
            end
         else
            #* key=:command_option_noaction  msg="command-line option '-f' or '-m' required."
            raise option_error(:command_option_noaction, @command)
         end
         return s   # or return (s == nil || s.empty?) ? nil : s
      end


      def self.main(command, argv=ARGV)
         begin
            main = Kwalify::Main.new(command)
            s = main.execute(argv)
            print s if s
         rescue Kwalify::CommandOptionError => ex
            $stderr.puts ex.message
            exit 1
         rescue Kwalify::KwalifyError => ex
            $stderr.puts "ERROR: #{ex.message}"
            exit 1
         #rescue => ex
         #   if main.debug?
         #      raise ex
         #   else
         #      $stderr.puts ex.message
         #      exit 1
         #   end
         end
      end


      private


      def option_error(error_symbol, arg)
         msg = Kwalify.msg(error_symbol) % arg
         return CommandOptionError.new(msg, arg, error_symbol)
      end


      def _find_template(action)
         template_filename = action + '.eruby'
         unless test(?f, template_filename)
            tpath = @template_path.find { |path| test(?f, "#{path}/#{template_filename}") }
            #* key=:command_option_notemplate  msg="%s: invalid action (template not found).\n"
            raise option_error(:command_option_notemplate, action) unless tpath
            template_filename = "#{tpath}/#{action}.eruby"
         end
         return template_filename
      end


      def _apply_template(template_filename, hash)
         template = File.read(template_filename)
         trim_mode = 1
         erb = ERB.new(template, $SAFE, trim_mode)
         context = Object.new
         hash.each do |key, val|
            context.instance_variable_set("@#{key}", val)
         end
         return context.instance_eval(erb.src, template_filename)
      end


      def _describe_properties(action)
         template_filename = _find_template(action)
         s = _apply_template(template_filename, :describe=>true)
         return s
      end


      def _perform_action(action, schema_filename, describe=false)
         template_filename = _find_template(action)
         schema = _load_schema_file(schema_filename)
         rule = Rule.new(schema)
         @properties[:schema_filename] = schema_filename
         s = _apply_template(template_filename, :rule=>rule, :properties=>@properties)
         return s
      end


      def _inspect_schema(schema_filename)
         filename = schema_filename
         str = File.open(schema_filename) { |f| f.read() }
         str = YamlUtil.untabify(str) if @options[:untabify]
         schema = YAML.load(str)
         return nil if schema == nil
         validator = Kwalify::Validator.new(schema)  # error raised when schema is wrong
         s = validator._inspect()
         s << "\n" unless s[-1] == ?\n
         return s
      end


      #--
      #class QuickMetaValidator
      #   def validate(schema)
      #      errors = []
      #      begin
      #         validator = Kwalify::Validator.new(schema) # error raised when schema is wrong
      #      rescue Kwalify::SchemaError => ex
      #         errors << ex
      #      end
      #      return errors
      #   end
      #end
      #++


      def _quick_meta_validate(filenames)
         meta_validator = Object.new
         def meta_validator.validate(schema)
            errors = []
            begin
               validator = Kwalify::Validator.new(schema) # error raised when schema is wrong
            rescue Kwalify::SchemaError => ex
               errors << ex
            end
            return errors
         end
         s = _validate_files(meta_validator, filenames)
         return s
      end


      def _load_schema_file(schema_filename)
         filename = schema_filename
         str = File.read(filename)
         str = YamlHelper.untabify(str) if @options[:untabify]
         schema = YAML.load(str)
         return schema
      end


      def _meta_validate(filenames)
         meta_validator = Kwalify::MetaValidator.instance()
         s = _validate_files(meta_validator, filenames)
         return s
      end


      def _validate(filenames, schema_filename)
         schema = _load_schema_file(schema_filename)
         if schema
            validator = Kwalify::Validator.new(schema)
            s = _validate_files(validator, filenames)
         else
            #* key=:schema_empty  msg="%s: empty schema.\n"
            s = Kwalify.msg(:schema_emtpy) % filename
         end
         return s
      end


      def _validate_files(validator, filenames)
         s = ''
         filenames = [ nil ] if filenames.empty?
         filenames.each do |filename|
            if filename
               str = File.open(filename) { |f| f.read() }   # or File.read(filename) in ruby1.8
            else
               str = $stdin.read()
               filename = '(stdin)'
            end
            str = YamlHelper.untabify(str) if @options[:untabify]
            if @options[:linenum]
               parser = Kwalify::YamlParser.new(str)
               i = 0
               while parser.has_next?
                  doc = parser.parse()
                  s << _validate_document(validator, doc, filename, i, parser)
                  i += 1
               end
            else
               parser = nil
               i = 0
               YAML.load_documents(str) do |doc|
                  s << _validate_document(validator, doc, filename, i, nil)
                  i += 1
               end
            end
         end
         return s
      end


      def _validate_document(validator, doc, filename, i, parser=nil)
         s = ''
         if doc == nil
            #* key=:validation_empty  msg="%s#%d: empty.\n"
            s << kwalify.msg(:validation_empty) % [filename, i]
            return s
         end
         errors = validator.validate(doc)
         if errors == nil || errors.empty?
            #* key=:validation_valid  msg="%s#%d: valid.\n"
            s << Kwalify.msg(:validation_valid) % [filename, i] unless @options[:silent]
         else
            #* key=:validation_invalid  msg="%s#%d: INVALID\n"
            s << Kwalify.msg(:validation_invalid) % [filename, i]
            if @options[:linenum]
               #assert parser != nil
               raise unless parser != nil
               parser.set_errors_linenum(errors)
               errors.sort!
            end
            errors.each do |error|
               if @options[:emacs]
                  #assert @options[:linenum]
                  raise unless @options[:linenum]
                  s << "#{filename}:#{error.linenum}: [#{error.path}] #{error.message}\n"
               elsif @options[:linenum]
                  s << "  - (line #{error.linenum}) [#{error.path}] #{error.message}\n"
               else
                  s << "  - [#{error.path}] #{error.message}\n"
               end
            end
         end
         return s
      end


      def _usage()
         #msg = Kwalify.msg(:command_help) % [@command, @command, @command]
         msg = Kwalify.msg(:command_help)
         return msg
      end


      def _version()
         return RELEASE
      end


      def _to_value(str)
         case str
         when nil, "null", "nil"         ;   return nil
         when "true", "yes"              ;   return true
         when "false", "no"              ;   return false
         when /\A\d+\z/                  ;   return str.to_i
         when /\A\d+\.\d+\z/             ;   return str.to_f
         when /\/(.*)\//                 ;   return Regexp.new($1)
         when /\A'.*'\z/, /\A".*"\z/     ;   return eval(str)
         else                            ;   return str
         end
      end


      def _parse_argv(argv)
         option_table = {
            ?h => :help,
            ?v => :version,
            ?s => :silent,
            ?t => :untabify,
            ?m => :meta,
            ?M => :meta2,
            ?E => :emacs,
            ?l => :linenum,
            ?f => :schema,
            ?D => :debug,
            ?a => :action,
            ?I => :tpath,
         }

         errcode_table = {
            #* key=:command_option_noschema  msg="-%s: schema filename required."
            ?f => :command_option_noschema,
            #* key=:command_option_noaction  msg="-%s: action required."
            ?a => :command_option_noaction,
            #* key=:command_option_notpath   msg="-%s: template path required."
            ?I => :command_option_notpath,
         }

         while argv[0] && argv[0][0] == ?-
            optstr = argv.shift
            optstr = optstr[1, optstr.length-1]
            # property
            if optstr[0] == ?-
               unless optstr =~ /\A\-([-\w]+)(?:=(.*))?\z/
                  #* key=:command_property_invalid  msg="%s: invalid property."
                  raise option_error(:command_property_invalid, optstr)
               end
               prop_name = $1;  prop_value = $2
               key   = prop_name.gsub(/-/, '_').intern
               value = prop_value == nil ? true : _to_value(prop_value)
               @properties[key] = value
            # option
            else
               while optstr && !optstr.empty?
                  optchar = optstr[0]
                  optstr[0,1] = ""
                  unless option_table.key?(optchar)
                     #* key=:command_option_invalid  msg="-%s: invalid command option."
                     raise option_error(:command_option_invalid, optchar.chr)
                  end
                  optkey = option_table[optchar]
                  case optchar
                  when ?f, ?a, ?I
                     arg = optstr.empty? ? argv.shift : optstr
                     raise option_error(errcode_table[optchar], optchar.chr) unless arg
                     @options[optkey] = arg
                     optstr = ''
                  else
                     @options[optkey] = true
                  end
               end
            end
         end  # end of while
         #
         @options[:linenum] = true if @options[:emacs]
         @options[:help]    = true if @properties[:help]
         @options[:version] = true if @properties[:version]
         filenames = argv
         return filenames
      end


      def _domain_type?(doc)
         klass = defined?(YAML::DomainType) ? YAML::DomainType : YAML::Syck::DomainType
         return doc.is_a?(klass)
      end

   end

end
#--end of require 'kwalify/main'

command = File.basename($0)
Kwalify::Main.main(command, ARGV)
