Kwalify User's Guide (for Ruby)

release: $Release: $

3   How to in Ruby

This section describes how to use Kwalify in Ruby.

3-1   Validation

require 'kwalify'
#require 'yaml'

## load schema data
schema = Kwalify::Yaml.load_file('schema.yaml')
## or
#schema = YAML.load_file('schema.yaml')

## create validator
validator = Kwalify::Validator.new(schema)

## load document
document = Kwalify::Yaml.load_file('document.yaml')
## or
#document = YAML.load_file('document.yaml')

## validate
errors = validator.validate(document)

## show errors
if errors && !errors.empty?
  for e in errors
    puts "[#{e.path}] #{e.message}"
  end
end

3-2   Parsing with Validation

From version 0.7, Kwalify supports parsing with validation.

require 'kwalify'
#require 'yaml'

## load schema data
schema = Kwalify::Yaml.load_file('schema.yaml')
## or
#schema = YAML.load_file('schema.yaml')

## create validator
validator = Kwalify::Validator.new(schema)

## create parser with validator
## (if validator is ommitted, no validation executed.)
parser = Kwalify:::Yaml::Parser.new(validator)

## parse document with validation
filename = 'document.yaml'
document = parser.parse_file(filename)
## or
#document = parser.parse(File.read(filename), filename)

## show errors if exist
errors = parser.errors()
if errors && !errors.empty?
  for e in errors
    puts "#{e.linenum}:#{e.column} [#{e.path}] #{e.message}"
  end
end

3-3   Meta Validation

Meta validator is a validator which validates schema definition. The schema definition is placed at 'kwalify/kwalify.schema.yaml'.

Kwalify also provides Kwalify::MetaValidator class which validates schema defition.

require 'kwalify'

## meta validator
metavalidator = Kwalify::MetaValidator.instance

## validate schema definition
parser = Kwalify::Yaml::Parser.new(metavalidator)
errors = parser.parse_file('schema.yaml')
for e in errors
  puts "#{e.linenum}:#{e.column} [#{e.path}] #{e.message}"
end if errors && !errors.empty?

Meta validation is also available with command-line option '-m'.

$ kwalify -m schema1.yaml schema2.yaml ...

3-4   Validator#validator_hook()

You can extend Kwalify::Validator and override Kwalify::Validator#validator_hook() method. This method is called by Kwalify::Validator#validate().

answers-schema.yaml : 'name:' is important.
type:      map
mapping:
 "answers":
    type:      seq
    sequence:
      - type:      map
        name:        Answer
        mapping:
         "name":   { type: str, required: yes }
         "answer": { type: str, required: yes,
                     enum: [good, not bad, bad] }
         "reason": { type: str }
answers-validator.rb : validate script for Ruby
#!/usr/bin/env ruby

require 'kwalify'

## validator class for answers
class AnswersValidator < Kwalify::Validator

   ## load schema definition
   @@schema = Kwalify::Yaml.load_file('answers-schema.yaml')
   ## or
   ##   require 'yaml'
   ##   @@schema = YAML.load_file('answers-schema.yaml')

   def initialize()
      super(@@schema)
   end

   ## hook method called by Validator#validate()
   def validate_hook(value, rule, path, errors)
      case rule.name
      when 'Answer'
         if value['answer'] == 'bad'
            reason = value['reason']
            if !reason || reason.empty?
               msg = "reason is required when answer is 'bad'."
               errors << Kwalify::ValidationError.new(msg, path)
            end
         end
      end
   end

end

## create validator
validator = AnswersValidator.new

## parse and validate YAML document
input = ARGF.read()
parser = Kwalify::Yaml::Parser.new(validator)
document = parser.parse(input)

## show errors
errors = parser.errors()
if !errors || errors.empty?
   puts "Valid."
else
   puts "*** INVALID!"
   for e in errors
      # e.class == Kwalify::ValidationError
      puts "#{e.linenum}:#{e.column} [#{e.path}] #{e.message}"
   end
end
document07a.yaml : valid document example
answers:
  - name:      Foo
    answer:    good
    reason:    I like this style.
  - name:      Bar
    answer:    not bad
  - name:      Baz
    answer:    bad
    reason:    I don't like this style.
validate
$ ruby answers-validator.rb document07a.yaml
Valid.
document07b.yaml : invalid document example
answers:
  - name:    Foo
    answer:  good
  - name:    Bar
    answer:  bad
  - name:    Baz
    answer:  not bad
validate
$ ruby answers-validator.rb document07b.yaml
*** INVALID!
4:3 [/answers/1] reason is required when answer is 'bad'.

You can validate some document by a Validator instance because Validator class and Validator#validate() method are stateless. If you use instance variables in custom validator_hook() method, it becomes to be stateful.

3-5   Preceding Alias

From version 0.7, Kwalify allows aliases to appear before corresponding anchors are now appeared. These aliases are called as 'preceding alias'.

howto3.yaml
- name: Foo
  parent: *bar        # preceding alias
- &bar
  name: Bar
  parent: *baz        # preceding alias
- &baz
  name: Baz
  parent: null

To enable preceding alias, set Kwalify::Yaml::Parser#preceding_alias to true.

howto3.rb
require 'kwalify'
parser = Kwalify::Yaml::Parser.new
parser.preceding_alias = true   # enable preceding alias
ydoc = parser.parse_file('howto3.yaml')
require 'pp'
pp ydoc
result
$ ruby howto3.rb
[{"name"=>"Foo",
  "parent"=>{"name"=>"Bar", "parent"=>{"name"=>"Baz", "parent"=>nil}}},
 {"name"=>"Bar", "parent"=>{"name"=>"Baz", "parent"=>nil}},
 {"name"=>"Baz", "parent"=>nil}]

Command-line option '-P' also enables preceding alias.

Preceding alias is very useful when document is complex.

3-6   Data Binding

From version 0.7, Kwalify supports data binding. * To enable data binding, set Kwlaify::Yaml::Parser#data_binding to true. * It is required to specify class name in schema definition. (Notice that 'class:' constraint is avaialbe only with rule which type is 'map'.) * Also instance methods '[]', '[]=', and 'keys?' must be defined in the classes. (Including Kwalify::Util::HashLike modules is easy way to define them.)

config.schema.yaml: schema definition file
type:  map
class: Config
mapping:
 "host": { type: str, required: true }
 "port": { type: int }
 "user": { type: str, required: true }
 "pass": { type: str, required: true }
config.yaml: data file
host:  localhost
port:  8080
user:  user1
pass:  password1
loadconfig.rb: ruby program
## class definition
require 'kwalify/util/hashlike'
class Config
  include Kwalify::Util::HashLike  # defines [], []=, and keys?
  attr_accessor :host, :posrt, :user, :pass
end
## create validator object
require 'kwalify'
schema = Kwalify::Yaml.load_file('config.schema.yaml')
validator = Kwalify::Validator.new(schema)
## parse configuration file with data binding
parser = Kwalify::Yaml::Parser.new(validator)
parser.data_binding = true    # enable data binding
config = parser.parse_file('config.yaml')
require 'pp'
pp config
result
$ ruby loadconfig.rb
#<Config:
 @host="localhost",
 @pass="password1",
 @port=8080,
 @user="user1">

Data binding is available even when data is more complex. Preceding alias is also available.

For example, the following data is complex because it uses anchor and alias (including preceding alias).

BABEL.data.yaml
teams:
  - &thechildren
    name:   The Children
    desc:   Level 7 ESPers
    chief:  *minamoto                  # preceding alias
    members:  [*kaoru, *aoi, *shiho]   # preceding aliases

members:
  - &minamoto
    name:   Kohichi Minamoto
    desc:   Scientist
    team:   *thechildren
  - &kaoru
    name:   Kaoru Akashi
    desc:   Psychokino
    team:   *thechildren
  - &aoi
    name:   Aoi Nogami
    desc:   Teleporter
    team:   *thechildren
  - &shiho
    name:   Shiho Sannomiya
    desc:   Psycometrer
    team:   *thechildren

Here is the schema definition. (Notice that 'class:' constraint is avaialbe only with rule which type is 'map'.)

BABEL.schema.yaml
type: map
required: yes
mapping:
 "teams":
    type: seq
    required: yes
    sequence:
      - &team
        type:  map
        required: yes
        class:  Team
        mapping:
         "name":  {type: str, required: yes, unique: yes}
         "desc":  {type: str}
         "chief":  *member       # preceding alias
         "members":
            type: seq
            sequence: [*member]  # preceding alias
 "members":
    type: seq
    required: yes
    sequence:
      - &member
        type:  map
        required: yes
        class:  Member
        mapping:
         "name":  {type: str, required: yes, unique: yes}
         "desc":  {type: str}
         "team":  *team

It is required to define class 'Team' and 'Member' for data-binding. Command-line option '-a genclass-ruby' will help you to generate class definitions from schema definition. Try 'kwalify -ha genclass-ruby' for more details about 'genclass-ruby' action.

$ kwalify -a genclass-ruby -P -f BABEL.schema.yaml \
    --hashlike --initialize=false --module=Babel
require 'kwalify/util/hashlike'

module Babel

  ## 
  class Team
    include Kwalify::Util::HashLike
    attr_accessor :name             # str
    attr_accessor :desc             # str
    attr_accessor :chief            # map
    attr_accessor :members          # seq
  end

  ## 
  class Member
    include Kwalify::Util::HashLike
    attr_accessor :name             # str
    attr_accessor :desc             # str
    attr_accessor :team             # map
  end

end
$ kwalify -a genclass-ruby -P -f BABEL.schema.yaml  \
    --hashlike --initialize=false --module=Babel > models.rb

Here is the ruby program.

loadbabel.rb
require 'kwalify'
require 'models'

## load schema definition
schema = Kwalify::Yaml.load_file('BABEL.schema.yaml',
                                 :untabify=>true,
                                 :preceding_alias=>true)

## add module name to 'class:'
Kwalify::Util.traverse_schema(schema) do |rulehash|
  if rulehash['class']
    rulehash['class'] = 'Babel::' + rulehash['class']
  end
end

## create validator
validator = Kwalify::Validator.new(schema)

## parse with data-binding
parser = Kwalify::Yaml::Parser.new(validator)
parser.preceding_alias = true
parser.data_binding = true
ydoc = parser.parse_file('BABEL.data.yaml', :untabify=>true)

## show document
require 'pp'
pp ydoc
result
$ ruby loadbabel.rb
{"teams"=>
  [#<Babel::Team:0x53e0f8
    @chief=
     #<Babel::Member:0x53d5e0
      @desc="Scientist",
      @name="Kohichi Minamoto",
      @team=#<Babel::Team:0x53e0f8 ...>>,
    @desc="Level 7 ESPers",
    @members=
     [#<Babel::Member:0x53d018
       @desc="Psychokino",
       @name="Kaoru Akashi",
       @team=#<Babel::Team:0x53e0f8 ...>>,
      #<Babel::Member:0x53ca50
       @desc="Teleporter",
       @name="Aoi Nogami",
       @team=#<Babel::Team:0x53e0f8 ...>>,
      #<Babel::Member:0x53c488
       @desc="Psycometrer",
       @name="Shiho Sannomiya",
       @team=#<Babel::Team:0x53e0f8 ...>>],
    @name="The Children">],
 "members"=>
  [#<Babel::Member:0x53d5e0
    @desc="Scientist",
    @name="Kohichi Minamoto",
    @team=
     #<Babel::Team:0x53e0f8
      @chief=#<Babel::Member:0x53d5e0 ...>,
      @desc="Level 7 ESPers",
      @members=
       [#<Babel::Member:0x53d018
         @desc="Psychokino",
         @name="Kaoru Akashi",
         @team=#<Babel::Team:0x53e0f8 ...>>,
        #<Babel::Member:0x53ca50
         @desc="Teleporter",
         @name="Aoi Nogami",
         @team=#<Babel::Team:0x53e0f8 ...>>,
        #<Babel::Member:0x53c488
         @desc="Psycometrer",
         @name="Shiho Sannomiya",
         @team=#<Babel::Team:0x53e0f8 ...>>],
      @name="The Children">>,
   #<Babel::Member:0x53d018
    @desc="Psychokino",
    @name="Kaoru Akashi",
    @team=
     #<Babel::Team:0x53e0f8
      @chief=
       #<Babel::Member:0x53d5e0
        @desc="Scientist",
        @name="Kohichi Minamoto",
        @team=#<Babel::Team:0x53e0f8 ...>>,
      @desc="Level 7 ESPers",
      @members=
       [#<Babel::Member:0x53d018 ...>,
        #<Babel::Member:0x53ca50
         @desc="Teleporter",
         @name="Aoi Nogami",
         @team=#<Babel::Team:0x53e0f8 ...>>,
        #<Babel::Member:0x53c488
         @desc="Psycometrer",
         @name="Shiho Sannomiya",
         @team=#<Babel::Team:0x53e0f8 ...>>],
      @name="The Children">>,
   #<Babel::Member:0x53ca50
    @desc="Teleporter",
    @name="Aoi Nogami",
    @team=
     #<Babel::Team:0x53e0f8
      @chief=
       #<Babel::Member:0x53d5e0
        @desc="Scientist",
        @name="Kohichi Minamoto",
        @team=#<Babel::Team:0x53e0f8 ...>>,
      @desc="Level 7 ESPers",
      @members=
       [#<Babel::Member:0x53d018
         @desc="Psychokino",
         @name="Kaoru Akashi",
         @team=#<Babel::Team:0x53e0f8 ...>>,
        #<Babel::Member:0x53ca50 ...>,
        #<Babel::Member:0x53c488
         @desc="Psycometrer",
         @name="Shiho Sannomiya",
         @team=#<Babel::Team:0x53e0f8 ...>>],
      @name="The Children">>,
   #<Babel::Member:0x53c488
    @desc="Psycometrer",
    @name="Shiho Sannomiya",
    @team=
     #<Babel::Team:0x53e0f8
      @chief=
       #<Babel::Member:0x53d5e0
        @desc="Scientist",
        @name="Kohichi Minamoto",
        @team=#<Babel::Team:0x53e0f8 ...>>,
      @desc="Level 7 ESPers",
      @members=
       [#<Babel::Member:0x53d018
         @desc="Psychokino",
         @name="Kaoru Akashi",
         @team=#<Babel::Team:0x53e0f8 ...>>,
        #<Babel::Member:0x53ca50
         @desc="Teleporter",
         @name="Aoi Nogami",
         @team=#<Babel::Team:0x53e0f8 ...>>,
        #<Babel::Member:0x53c488 ...>],
      @name="The Children">>]}