SYNOPSIS

Outthentic DSL

Outthentic DSL

  • DSL provides some meta language to validate arbitrary plain text.

  • One should create a so called `check files' - DSL scripts to describe validation process.

  • Outthentic DSL is both imperative and declarative language.

  • It's convenient to refer to the text validate by as `stdout', thinking that one program generates and yields some output into stdout.

Check files

Check file is a regular file in text plain format. The content of check file is a DSL script.

Parser

`Parser' is the program which:

  • parses check file line by line

  • creates and then executes outthentic entry represented by parsed line(s)

  • execution of entry results in one of three things:

    • validation stdout against check expression - if entry is check expression one

    • generating new outhentic entries - if entry is generator one

    • execution of perl code - if entry is perl expression one

Outthentic entries

Outhentic DSL comprises following basic entities, listed at pretty arbitrary order:

  • check expressions:

    • plain strings

    • regular expressions

    • text blocks

    • within expressions

  • comments

  • blank lines

  • perl expressions

  • generators

Check expressions

Check expressions defines what lines stdout should have:

# stdout
HELLO
HELLO WORLD
My birth day is: 1977-04-16


# check list
HELLO
regexp: \d\d\d\d-\d\d-\d\d


# validator output
OK - output matches "HELLO"
OK - output matches /\d\d\d\d-\d\d-\d\d/

There are two type of check expressions - plain strings and regular expressions.

It is convenient to talk about check list as of all check expressions in a given check file.

plain string

I am ok
HELLO Outthentic

The code above declares that stdout should have lines 'I am ok' and 'HELLO Outthentic'.

regular expressions

Similarly to plain strings matching, you may require that stdout has lines matching the regular expressions:

regexp: \d\d\d\d-\d\d-\d\d # date in format of YYYY-MM-DD
regexp: Name: \w+ # name
regexp: App Version Number: \d+\.\d+\.\d+ # version number

Regular expressions should start with `regexp:' marker.

captures

Parser does not care about how many times a given check expression is found in stdout.

It's only required that at least one line in stdout match the check expression ( this is not the case with text blocks, see later )

However it's possible to accumulate all matching lines and save them for further processing:

regexp: (Hello, my name is (\w+))

See "captures" section for full explanation of a captures mechanism:

Comments, blank lines and text blocks

Comments and blank lines don't impact validation process but one could use them to improve code readability.

  • comments

Comment lines start with `#' symbol, comments chunks are ignored by parser:

# comments could be represented at a distinct line, like here
The beginning of story
Hello World # or could be added to existed expression to the right, like here
  • blank lines

Blank lines are ignored as well:

# every story has the beginning
The beginning of a story
# then 2 blank lines


# end has the end
The end of a story

But you can't ignore blank lines in a `text block matching' context ( see `text blocks' subsection ), use `:blank_line' marker to match blank lines:

# :blank_line marker matches blank lines
# this is especially useful
# when match in text blocks context:

begin:
    this line followed by 2 blank lines
    :blank_line
    :blank_line
end:
  • text blocks

Sometimes it is very helpful to match a stdout against a `block of strings' goes consequentially, like here:

# this text block
# consists of 5 strings
# goes consequentially
# line by line:

begin:
    # plain strings
    this string followed by
    that string followed by
    another one
    # regexps patterns:
    regexp: with (this|that)
    # and the last one in a block
    at the very end
end:

This validation will succeed when gets executed against this chunk:

this string followed by
that string followed by
another one string
with that string
at the very end.

But will not for this chunk:

that string followed by
this string followed by
another one string
with that string
at the very end.

`begin:' `end:' markers decorate `text blocks' content. `:being|:end' markers should not be followed by any text at the same line.

Also be aware if you leave "dangling" `begin:' marker without closing `end': somewhere else

Parser will remain in a `text block' mode till the end of check file, which is probably not you want:

begin:
    here we begin
    and till the very end of test
    we are in `text block` mode

Perl expressions

Perl expressions are just a pieces of perl code to get evaled during parsing process. This is how it works:

# perl expression between two check expressions
Once upon a time
code: print "hello I am Outthentic"
Lived a boy called Outthentic

Internally once check file gets parsed this piece of DSL code is "turned" into regular perl code:

ok($status,"stdout matches Once upon a time");
eval 'print "Lived a boy called Outthentic"';
ok($status,"stdout matches Lived a boy called Outthentic");

So, all perl expressions in DSL code will be replaced by perl eval {code} expressions.

Example with 'print "Lived a boy called Outthentic"' is quite useless, here some more realistic examples:

# use of Test::More functions
# to modify validation workflow:

# skip tests

code: skip('next 3 checks are skipped',3) # skip three next checks forever
color: red
color: blue
color: green

number:one
number:two
number:three

# skip tests conditionally

color: red
color: blue
color: green

code: skip('numbers checks are skipped',3)  if $ENV{'skip_numbers'} # skip three next checks if skip_numbers set

number:one
number:two
number:three

Perl expressions could be effectively used with `captures':

regexp: my name is (\w+) and my age is (\d+)
code: cmp_ok(capture()->[1],'>',20,capture()->[0].' is adult one');

Generators

  • Generators is the way to generate new outthentic entries on the fly.

  • Generators like perl expressions are just a piece of perl code.

  • The only requirement for generator code - it should return reference to array of strings.

  • Strings in array returned by generator code represent new outthentic entities.

  • An array items are passed back to parser, so parser generate news outthentic entities and execute them.

  • Generators expressions start with `:generator' marker.

Here is simple example:

# original check list

Say
HELLO
 
# this generator creates 3 new check expressions:

generator: [ qw{ say hello again } ]


# final check list:

Say
HELLO
say
hello
again

Here is more complicated example:

# this generator generates
# comment lines
# and plain string check expressions:

generator: my %d = { 'foo' => 'foo value', 'bar' => 'bar value' }; [ map  { ( "# $_", "$data{$_}" )  } keys %d ]

# generated entries:

# foo
foo value
# bar
bar value

multiline expressions

When generate and execute check expressions parser operates in a single line mode :

  • check expressions are treated as single line strings

  • stdout is validated by given check expression in line by line way

For example:

# check list
# consists of
# single line entries

Multiline
string
here
regexp: Multiline \n string \n here

# stdout
Multiline \n string \n here
 
 
# validation output
OK output matches "Multiline"
OK output matches "string"
OK output matches "here"
NOT_OK output matches "Multiline \n string \n here"

Use text blocks if you want to achieve multiline checks.

However when writing perl expressions or generators one could use multilines strings.

`\' delimiters breaks a single line text on a multi lines:

# What about to validate stdout
# With sqlite database entries?

generator:                                                          \

use DBI;                                                            \
my $dbh = DBI->connect("dbi:SQLite:dbname=t/data/test.db","","");   \
my $sth = $dbh->prepare("SELECT name from users");                  \
$sth->execute();                                                    \
my $results = $sth->fetchall_arrayref;                              \

[ map { $_->[0] } @${results} ]

Captures

Captures are pieces of data get captured when parser validates stdout against a regular expressions:

# stdout
# it's my family ages.
alex    38
julia   25
jan     2


# let's capture name and age chunks
regexp: /(\w+)\s+(\d+)/

After this regular expression check gets executed captured data will stored into a array:

[
    ['alex',    38 ]
    ['julia',   32 ]
    ['jan',     2  ]
]

Then captured data might be accessed for example by code generator to define some extra checks:

code:                               \
my $total=0;                        \
for my $c (@{captures()}) {         \
    $total+=$c->[0];                \
}                                   \
cmp_ok( $total,'==',72,"total age of my family" );
  • `captures()' function is used to access captured data array,

  • it returns an array reference holding all chunks captured during latest regular expression check.

Here some more examples:

# check if stdout contains numbers,
# then calculate total amount
# and check if it is greater then 10

regexp: (\d+)
code:                               \
my $total=0;                        \
for my $c (@{captures()}) {         \
    $total+=$c->[0];                \
}                                   \
cmp_ok( $total,'>',10,"total amount is greater than 10" );


# check if stdout contains lines
# with date formatted as date: YYYY-MM-DD
# and then check if first date found is yesterday

regexp: date: (\d\d\d\d)-(\d\d)-(\d\d)
code:                               \
use DateTime;                       \
my $c = captures()->[0];            \
my $dt = DateTime->new( year => $c->[0], month => $c->[1], day => $c->[2]  ); \
my $yesterday = DateTime->now->subtract( days =>  1 );                        \
cmp_ok( DateTime->compare($dt, $yesterday),'==',0,"first day found is - $dt and this is a yesterday" );

You also may use `capture()' function to get a first element of captures array:

# check if stdout contains numbers
# a first number should be greater then ten

regexp: (\d+)
code: cmp_ok( capture()->[0],'>',10,"first number is greater than 10" );

AUTHOR

Aleksei Melezhik

Home Page

https://github.com/melezhik/outthentic-dsl

COPYRIGHT

Copyright 2015 Alexey Melezhik.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

Thanks

  • to God as - For the LORD giveth wisdom: out of his mouth cometh knowledge and understanding. (Proverbs 2:6)