Parspec
A gUnit-like specification language for Parslet parsers and transformers, which gets translated to RSpec specs.
Status
This software is feature-complete, since it realizes the features of gUnit (with some additions) completely and can successfully be used as a full gUnit-like replacement of handmade RSpec specs, while still using the excellent RSpec testing platform, allowing to integrate the generated specs with handmade RSpec specs.
Although some nice language additions are thinkable, I won't implement them in the near future, until I need them.
Installation
Add this line to your application's Gemfile:
gem 'parspec', group: :test
And then execute:
$ bundle
Or install with gem:
$ gem install parspec
Specification language
Example
The following example shows parts of the TOML spec translated to Parspec:
parser TOML::Parser
value:
"120381" OK
"0181" FAIL
"3.14159" OK
".1" FAIL
"true" OK
"truefalse" FAIL
"1979-05-27T07:32:00Z" OK
"1979l05-27 07:32:00" FAIL
"\"hello world\"" OK
"\"hello\nworld\"" FAIL
"\"hello/world\"" FAIL
"1234" -> ":integer => '1234'"
"-0.123" -> ":float => '-0.123'"
"true" -> ":boolean => 'true'"
"1979-05-27T07:32:00Z" -> ":datetime => '1979-05-27T07:32:00Z'"
"\"hello world\"" -> ":string => 'hello world'"
"\"hello\nworld\"" -> ":string => \"hello\nworld\""
array:
"[]" OK
"[1]" OK
"[0.1, -0.1, 3.14159]" OK
"[ true, false, true, true ]" OK
"[1979-05-27T07:32:00Z]" OK # [2013-02-24T17:26:21Z]
"[\n1\n,\n2\n]" OK
"[\n\n\t1 , 2, 3\\t,4\n]" OK
"[1, 2, \"three\"]" FAIL
"[1,2,]" OK
"[1,2\n,\\t]" OK
"[1,2]" -> ":array => [ {:integer => '1'}, {:integer => '2'}]"
"[]" -> ":array => '[]'"
"[ [1,2] ]"
-> ":array => [
{:array => [ {:integer => '1'}, {:integer => '2'}]}
]"
key:
"foobar" OK
"lolwhat.noWAY" OK
"no white\\tspace" FAIL
"noequal=thing" FAIL
assignment:
"key=3.14" OK
"key = 10" OK
"key = true" OK
"key = \"value\"" OK
"#comment=1" FAIL
"thing = 1" -> ":key => 'thing', :value => {:integer => '1'}"
Description
Header
A Parspec specification starts with a definition of the subject:
<type> <instantiation expression>
There are two types of specifications: parser
and transformer
. A parser specification describes a Parslet::Parser
, a transformer specification describes a Parslet::Transform
. Syntactically these types of specifications are equal, but the generated RSpec descriptions will differ.
The instantiation expression is used to create a RSpec test subject. It usually consists of a constant for a Parslet::Parser
or Parslet::Transform
class, according to the type of specification, but can be any valid Ruby expression, which responds to new
with a Parslet::Parser
or Parslet::Tranform
instance.
Rule examples
After the definition, a series of examples for the grammar rules follows. Rules start with a rule name followed by a colon.
There are two types of examples: validations and mapping examples.
Validations
A validation is a string in double-quotes followed either by the keyword OK
or FAIL
, according to the expected outcome of parsing the given string under the given rule. Currently, it is supported in parser specifications only.
For example, the following validation:
some_rule:
"some input" OK
"another input" FAIL
will translate to this RSpec description:
context 'some_rule parsing' do
subject { parser.some_rule }
it { should parse 'some input' }
it { should_not parse 'another input' }
end
Mapping examples
Mapping examples describe the input-output-behaviour of a rule. Syntactically they consist of two strings separated by ->
. Since the semantics of parser and transformer specifications differ, let's discuss them separately, starting with the parser case:
While the input string on the left side is simply some sample text as in a validity example, the output string on the right must contain a valid Ruby expression, which should evaluate to the expected outcome of the respective rule parsing.
For example, the following mapping:
some_rule:
"some input" -> "42"
"another input" -> "{ foo: 'bar' }"
will be translated to the following RSpec parser specification:
context 'some_rule parsing' do
subject { parser.some_rule }
it "should parse 'some input' to 42" do
expect(subject.parse('some input')).to eq 42
end
it "should parse 'another input' to { foo: 'bar' }" do
expect(subject.parse('another input')).to eq { foo: 'bar' }
end
end
In the case of a transformer specification, both sides must contain Ruby expressions.
Shared examples
The examples of a rule can be reused inside other rules with the include
keyword:
some_rule:
"some input" -> "42"
"another input" -> "{ foo: 'bar' }"
another_rule:
include some_rule
String escaping
Parspec strings in general support the following escape sequences: \t
, \n
, \r
, \"
, \\
.
Comments
One-line comments are supported in the #
-style.
Usage
Command-line interface
Parspec comes with a command-line interface through the parspec
command.
For a full description of the available parameters, run:
$ parspec --help
Usage: parspec [options] PARSPEC_FILE
-h, --help Display this information
-v, --version Print version information
-s, --stdout Print the translation to stdout only
-o, --out OUTPUT_FILE Path where translated RSpec file should be stored
-b, --beside-input Put the output file into the same directory as the input
-e, --header HEADER A block of code to be put in front of the translation
--no-debug-parse Don't print the whole Parslet ascii_tree on errors
Unless specified otherwise, the default header is:
# coding: utf-8
require 'spec_helper'
require 'parslet/convenience'
require 'parslet/rig/rspec'
load_parspec
You can use the command-line interface to integrate Parspec in your testing tool chain, e.g. via Rake or Guard.
But you can also load your Parspec spec from a normal Ruby file in your spec directory with the load_parspec
command:
require 'spec_helper'
require 'parspec'
require 'my_parser'
load_parspec __FILE__
If the load_parspec
command gets a filename with the extension .rb
, it looks for a file with the same name, but the extension .parspec
. For example, if the former Ruby file would be at spec/my_parser/my_parser_spec.rb
, the load_parspec
command would try to load a Parspec spec from a file spec/my_parser/my_parser_spec.parspec
.
Note: This feature is currently implemented via eval
, till I find a way to include specs from other RSpec files or another alternative. If you have any advice, please share it in issue #1.
Contributing
- Fork it ( https://github.com/marcelotto/parspec/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Author
- Marcel Otto