We will consider a very simple object model specification file consisting of a list of classes, each of which contains a list of typed attributes:
// File: emp.om (om stands for object model) class Employee { int id; string name; int dept_id; }; class Department { int id; string name; };
From this specification, we wish to produce a C++ header file for each class. Assume, for example, that the file Employee.h is expected to look like this (and similarly for Department.h):
#ifndef _Employee_h_ #define _Employee_h_ #include <Object.h> // File : 'Employee.h' // User : "sriram" class Employee : Object { int id; string name; int dept_id; Employee(); // private constructor. Use Create() public: // Methods Employee* Create(); ~Employee(); // Accessor Methods; int get_id(); void set_id(int); string get_name(); void set_name(string); int get_dept_id(); void set_dept_id(int); } #endif
Instead of succumbing to the temptation of writing a throwaway script to handle this specific job, we use Jeeves. This approach has three steps:
Invoke Jeeves with the name of the specification parser, the template file, and the example specification file.
This approach forces you to separate the parsing and output stages into two different modules. You might think it is simpler to write a throwaway script, but that's not quite true: you still have the problem of parsing the specification and producing the output. If you play by Jeeves's rules, you can take advantage of its template-processing facility. Jeeves expects the parser to boil the specification down to a data structure known as an abstract syntax tree (AST). Jeeves does not help you with parsing; after all, how can it know about a language we randomly cooked up not too long ago?
The syntax tree is a simple hierarchical arrangement of properties and property lists and is shown in Figure 17.1 for our example problem. Shaded boxes represent AST nodes, and outer boxes represent collections of these nodes (vector properties). Each node in this syntax tree has one or more properties (or name-value pairs). A property value is either a scalar (class_name, attr_name, attr_type) or a vector of other nodes (attr_list and class_list). As currently implemented, Jeeves does not expect nodes to contain any other type of values (references to other types of arrays, or to hashes, for example).
To get a quick overview of the Jeeves process, we will assume for now that the input specification parser has already been written and is capable of producing the syntax tree in Figure 17.1. The implementation is explained later, in the section "Sample Specification Parser."
The next step is to write a template file (call it oo.tpl) to output the requested files. Jeeves allows you to use the properties in the syntax tree as variables and provides keywords to iterate through vector properties. The template in Example 17.1 produces the two files in one fell swoop.
@foreach class_list @//------------------------------------------------------------------ @// Note: we are opening a new ".h" file within the foreach above ... @openfile ${class_name}.h #ifndef _${class_name}_h_ #define _${class_name}_h_ #include <Object.h> @perl $user = $ENV{"USER"}; // File : '${class_name}.h' // User : "$user" class $class_name : Object { @foreach attr_list $attr_type $attr_name; @end $class_name(); // private constructor. Use Create() public: // Methods $class_name* Create(); ~$class_name(); // Accessor Methods; @foreach attr_list $attr_type get_${attr_name}(); void set_${attr_name}($attr_type); @end .. attr_list } #endif @end .. class_list
The template file contains a mixture of control statements (the highlighted lines starting with @), attributes (indicated by $varname), and ordinary text. The highlighted lines are indented to indicate nesting of control structures. Ordinary text is simply output after variable interpolations, and whitespace is faithfully preserved.
It is important for the template writer to know the kind of syntax tree that is being produced by the input parser and the set of properties at each type of node. In the preceding example, the template writer has to know that the list of classes is known as class_list, each element of which has properties such as class_name and attr_list.
Ordinarily, a line in the template is simply interpolated (all scalar variables are expanded in situ) and written to the file last opened by the @openfile construct. If the line belongs inside an @foreach ... @end block, it gets interpolated and written several times. The @foreach block iterates through a list-valued property in the syntax tree and makes the properties of the current AST node available as global variables. For example, @foreach class_list "visits" each node pointed to by the class_list property and makes the variables $class_name and $attr_list (refer to Figure 17.1) available for the text following the @foreach directive. In the sample template shown earlier, because @open_file is within such a block and uses the variable $class_name for the filename, the template produces a new file during each iteration. Ordinary template lines are simply funneled into the currently open file. The @perl command allows you to intersperse Perl code when the built-in primitives don't quite cut it. We'll cover some more template directives when we discuss the template parser implementation.
Having written the object model specification parser, OO_Schema.pm, the template oo.tpl, and our example specification, emp.om, we invoke Jeeves as follows:
% jeeves -s OO_Schema -t oo.tpl emp.om Translated oo.tpl to oo.tpl.pl Parsed emp.om % ls *.h Department.h Employee.h
This template is now capable of generating C++ code for any class in your specification. One small change in the template can be instantly reflected in all pieces of code.
As soon as you finish doing this and are ready to go home, your remarkably prescient boss comes in and asks you to generate one more file: an SQL script to create the corresponding relational database schema. The script, db.sql, is expected to look like this:
create table Employee ( id integer, name varchar, dept_id integer, ) create table Department ( id integer, name varchar, )
Luckily, the Jeeves template makes this a two-minute task. Just create one more template file (or add the piece in Example 17.2 to the earlier template).
@openfile db.sql @perl %db_typemap = ("int" => 'integer', string => 'varchar'); create table $class_name ( @foreach attr_list @perl my $db_type = $db_typemap{$attr_type}; $attr_name $db_type, @end ) @end .. class_list
The template maps each attribute's type to the corresponding SQL datatype, using a snippet of Perl code.
As you can see, this architecture allows us to reuse specification parsers; we have used the information generated by the parser to fashion a completely different output.