Is a command line tool that allows the user to generate files from user made templates with arbitrary data made by the user as well.
The data source is preferably a json
object or a json
file. If you'd like to provide a data source from a different file, then you will have to write your own parser (mtml
allows you to provide a custom parser).
The templates use ejs for rendering. To prevent a collision with the current ejs
syntax, mtml
uses the dollar sign <$ $>
brackets.
- Provide a quick way to create a substantial amount of code whenever there exists a pattern (pretty often, right?)
- Use familiar syntax and file types: NodeJS, Javascript, JSON, EJS
- Simple functionality, plenty of flexibility
npm install -g mtml
Remember the -g
flag so that the application will be available from the command line anywhere in your file system. On *nix you might have to sudo
it ;)
mtml my-file.scenario.mtml any-additional command-line-args
The scenario file is mandatory, any additional command line argument's functionality is described by the developer in the scenario file and the templates
Let's say that we are working on a back-end in NestJS. The requirement is to build an API endpoint for each database's entity. The back-end database uses TypeORM as a wrapper. To keep things simple for this tutorial, the back-end code is very simplified and lacks imports, etc.
Make sure that you've installed mtml
.
Create an empty folder to follow this small tutorial.
NOTE: All of the below setup files are available in the examples
folder.
Lets define our first entity. Create a file inside of the folder you've just created and call it user.json
:
{
"name": "User",
"data": [
{ "field": "id", "type": "number" },
{ "field": "name", "type": "string" },
{ "field": "age", "type": "number" }
]
}
A simple enough entity called User. It has three fields, each of a certain name and type.
It is purely up to the developer to structure the entity and define the data that goes into it. The above is just a basic example. mtml
does not limit you to CRUD operations only.
Now lets define templates for creating the database TypeORM entity, creating the database service and creating the API endpoint (as stated above). In your case it could be: SQL files, PHP files, Java files, Django, Python, HTML files, ...
Keep in mind that the below template's created code syntax may not be accurate or structurally sound, it is just to showcase how mtml
works.
Create a file inside of your project's folder and name it db-entity.template.mtml
:
@Entity()
export class <$= e.name $> {
<$_ e.data.forEach(function(item) { _$>
<$_ if (item.field === 'id') { _$>
@PrimaryGeneratedColumn()
<$_ } else { _$>
@Column()
<$_ } _$>
<$= item.field $>: <$= item.type $>;
<$_ }) _$>
}
Note the e
variable. e
refers to your entity that you defined in the user.json
file.
Breakedown: Name the class the same way as you named your entity. Then iterate through the data array: for each element of the array create a new section for TypeORM field description. If the field is called id then make it a primary column, otherwise just make a normal column.
Note the usage of underscores <$_ _$>
to collapse blank lines.
Create a file inside of your project's folder and name it: db-service.template.mtml
.
<$_ var entityName = e.name _$>
<$_ var lEntityName = v.decapitalize(e.name) _$>
export class Db<$= entityName $>Service {
constructor(
@InjectRepository(<$= entityName $>)
private readonly <$= lEntityName $>Repository: Repository<<$= entityName $>>,
) { }
async find(query?: any): Promise<<$= entityName $>[]> {
let <$= lEntityName $>s: <$= entityName $>[];
try {
<$= lEntityName $>s = await this.<$= lEntityName $>Repository.find(query);
} catch {
throw new ServerErrorException();
}
return <$= lEntityName $>s;
}
async save(<$= lEntityName $>: <$= entityName $>): Promise<<$= entityName $>> {
try {
<$= lEntityName $> = await this.<$= lEntityName $>Repository.save(<$= lEntityName $>);
} catch {
throw new ServerErrorException();
}
return <$= lEntityName $>;
}
}
Breakdown: define two variables (for ease of use) entityName
and a lower case version of it lEntityName
(v.decapitalize
references the decapitalize
method from the voca package, this package is provided in mtml
). The two defined variables are then used throughout the rest of the template to insert either a lowercase or a capitalized version of the entity name into the template file.
The template file itself is simply a database service for finding and saving entity data in the database.
Create a file inside of your project's folder and name it: rest-endpoint.template.mtml
.
<$_ var entityName = e.name _$>
<$_ var lEntityName = v.decapitalize(e.name) _$>
@Controller('api/<$= lEntityName $>')
export class <$= entityName $>Controller {
constructor(
private readonly <$= lEntityName $>Service: Db<$= entityName $>Service
) { }
@Get()
async <$= lEntityName $>s(): Promise<<$= entityName $>[]> {
return await this.<$= lEntityName $>Service.find();
}
@Post()
async <$= lEntityName $>Post(@Body() <$= lEntityName $>: <$= entityName $>) {
return await this.<$= lEntityName $>Service.save(<$= lEntityName $>);
}
}
As with the previous template we define two convenience variables entityName
and lEntityName
. The rest is straightforward and simply defines two endpoints for the User entity: GET api/user
and POST api/user
. The endpoint uses the database service that we have created previously.
Create a file inside of your project's folder and name it: my-project.scenario.mtml
.
{
"entity": {
"json": "h.getArg(1)"
},
"template": [
{
"name": "dbEntity",
"from": "db-entity.template.mtml"
},
{
"name": "dbService",
"from": "db-service.template.mtml"
},
{
"name": "apiEndpoint",
"from": "rest-endpoint.template.mtml"
}
],
"use": [
{
"template": "dbEntity",
"spawn": "`db/entity/${v.decapitalize(e.name)}/${v.decapitalize(e.name)}.entity.ts`"
},
{
"template": "dbService",
"spawn": "`db/service/${v.decapitalize(e.name)}/${v.decapitalize(e.name)}.service.ts`"
},
{
"if": "true",
"template": "apiEndpoint",
"spawn": "`rest/${v.decapitalize(e.name)}/${v.decapitalize(e.name)}.controller.ts`"
}
]
}
This scenario file declares:
- Where to take the entity from
- How to read the entity
- Where to get the templates from and what are their names
- How to use the templates and what files to spawn out of them
Breakdown of the above scenario file:
- Provide the entity as
json
, the json file is given as the 1st argument from the command line (h.getArg(1)
).h
is a helper package with a few convenience functions. - Provide an array of templates, each template has its own name and the file path where it can be found. As an example, the first template is named
dbEntity
and can be located atdb-entity.template.mtml
. Thename
andfrom
keys are mandatory! - Provide an array of uses, each use takes the template, injects the entity into it and spawns a file in the given location. As an example, the first use takes the
dbEntity
and spawns a file atdb/entity/user/user.entity.ts
. We use string interpolation and thevoca
package'sdecapitalize
method to manipulate the path. Each of the uses can optionally have anif
key, if it evaluates to false, then that particular use will be skipped. NOTE: all pathing withinmtml
is relative to the scenario file by default.
The keys: entity
, template
and use
are mandatory.
mtml
uses eval
quite heavily to provide flexibility of the scenario file. This is perhaps the first time ever that I saw a good use of eval
when creating a project. I know that eval
has security issues, but mtml
is meant to be used by the developer that created the scenario file and thus it is his/hers responsibility to not break things and use mtml
with caution.
Each of the values of the keys are first eval'ed
, if the eval
fails then the value is taken as is. So if you type in as a value: v.capitalize('hello')
, then the value will be Hello
(v
references the voca
package), but if you type x.capitalize('hello')
, then the eval
will fail and the value will literally be the string x.capitalize('hello')
, since x
is not a known variable.
Convenience methods and variables that can be used as the values of the keys are described here.
Now, since we have completed the setup, let's run the following command from within the project folder that you've created:
mtml my-project.scenario.mtml user.json
You should see a list of three files that have been created: user.entity.ts
, user.service.ts
and user.controller.ts
.
Do you remember the h.getArg(1)
in the scenario file's json
key? That method grabs the 1st argument of the command line, in this case user.json
. By doing so we can now pass in different json
files as entities from the command line to mtml
and reuse the scenario!
Lets create a different entity, create a product.json
file:
{
"name": "Product",
"description": "Product creation API",
"data": [
{ "field": "id", "type": "number" },
{ "field": "name", "type": "string" },
{ "field": "description", "type": "string" },
{ "field": "price", "type": "number"}
]
}
So now if you run:
mtml my-project.scenario.mtml product.json
You will reuse your scenario file and all the templates that you've created, but with a different data input (the product.json
file). Neat, right?
It is purely up to you how far you want your entity to expand. You could create a whole back-end/front-end rest API and the relevant html
files and their inputs fields. All based from the entity file. It is all up to your imagination where this takes you and how much it helps you.
By default pathing in 'mtml' is relative to the scenario file. To override this behaviour add a relativeTo
key to the entity
, template
or use
objects. There are two possible values "scenario"
(the default) or "cwd"
. "cwd"
loads or spawns files relative to where mtml
was called ((c)urrent (w)orking (d)irectory).
For example, with the folder structure:
|-- mtml/
| |-- templates/
| | |-- db-entity.template.mtml
| | |-- db-service.template.mtml
| | `-- rest-endpoint.template.mtml
| `-- my-project.scenario.mtml
`-- user.json
Changing the above scenario file to:
{
"entity": {
"json": "h.getArg(1)",
"relativeTo": "cwd"
},
"template": [
{
"name": "dbEntity",
"from": "templates/db-entity.template.mtml",
},
{
"name": "dbService",
"from": "templates/db-service.template.mtml",
},
{
"name": "apiEndpoint",
"from": "templates/rest-endpoint.template.mtml",
}
],
"use": [
{
"template": "dbEntity",
"spawn": "`db/entity/${v.decapitalize(e.name)}/${v.decapitalize(e.name)}.entity.ts`",
"relativeTo": "cwd"
},
{
"template": "dbService",
"spawn": "`db/service/${v.decapitalize(e.name)}/${v.decapitalize(e.name)}.service.ts`",
"relativeTo": "cwd"
},
{
"template": "apiEndpoint",
"spawn": "`rest/${v.decapitalize(e.name)}/${v.decapitalize(e.name)}.controller.ts`",
"relativeTo": "cwd"
}
]
}
Would result in the following after running
$ mtml mtml/my-project.scenario.mtml user.json
|-- db/
| |-- entity/
| | `-- user/
| | `-- user.entity.ts
| `-- service/
| `-- user/
| `-- user.service.ts
|-- rest/
| `-- user/
| `-- user.controller.ts
|-- mtml/
| |-- templates/
| | |-- db-entity.template.mtml
| | |-- db-service.template.mtml
| | `-- rest-endpoint.template.mtml
| `-- my-project.scenario.mtml
`-- user.json
Both the template file and the scenario file have access to the following convenience variables, and their methods/objects:
- Built-in JS methods/functions work as intended in both templates and the scenario files, e.g.
parseInt
,JSON.parse
, etc. v
the voca package and all of its methods_
the lodash package and all of its methodss
is the whole scenario object after all the values have been evaluated. This means that it contains themeta
,entity
,template
anduse
keys.e
is the scenario's entity object after all the values have been evaluated. It is simply a shortcut tos.entity
.m
is the scenario's meta object after all the values have been evaluated. Is is simply a shortcut tos.meta
.h
the custom helpers lib which has the following methods:abort(reason)
aborts the application's execution, pass in a string as thereason
askUser(prompt)
ask for input from the user via the command line, provide a string as theprompt
getArg(number)
get the command line argument provided at indexnumber
. Example: if you would run from the command linemtml my.scenario.mtml foo.json
, thenmy.scenario.mtml
is argument 0 andfoo.json
is argument 1.
Two command line arguments are available:
--dry-run
will display where the spawned files would have been created without actually modifying any data on the file system--debug
will show some debug data about evaluated scenario file and the command line arguments, very useful for troubleshooting
Please note that providing the above command line arguments makes them NOT available via h.getArg
method. Running mtml my.scenario.mtml foo.json --dry-run
does not mean that h.getArg(2)
will return --dry-run
, it will be undefined
. Still, for simplicity's sake, provide the above command line arguments as last arguments.
The scenario file can also contain additional meta:
{
"meta": {
"description": "h.askUser('Provide a description:')"
},
"entity": {
"json": "h.getArg(1)"
},
"template": [
{
"name": "dbEntity",
"from": "db-entity.template.mtml"
}
],
"use": [
{
"template": "dbEntity",
"spawn": "`db/entity/${v.decapitalize(e.name)}/${v.decapitalize(e.name)}.entity.ts`"
}
]
}
In the above case the h.askUser
method will be called upon execution of the scenario file and the user will be asked to Provide a description:
. Once a user enters the value from the command prompt it will be stored in the meta
's description
key. To use the value in the the scenario file or the templates, reference it with the m
convenience variable. E.g. a template that creates a html header with the description provided from the command line
<h1><$= m.descripton $></h1>
In the examples above we provide the entity from a JSON file. But, in-fact there are three different ways of providing an entity. The entity key must have one of the three attributes:
json
as in the example above, a JSON filehere
a JSON object placed directly in the scenario fileparser
an arbitrary file source. This option will need a parser written by you.
Is alread explained here. However, the syntax is:
"entity": {
"json": "path to a file"
}
You can use a raw string, or use h.askUser
to ask the user for a path from the command line, or h.getArg
to provide a path from the command line. Remember that the path is relative to the scenario file.
This option is simple, the syntax looks as follows:
"entity": {
"here": [ {"my": "array"}, {"of": "objects"}]
}
Now the convenience variable e
will contain an array of two objects one {"my": "array"}, and the other {"of": "objects"}.
In my case I inherited a project in which all the mongoose files were already written, so I didn't exactly want to write the JSON entities anew, so I thought that I'd write a parser for the mongoose files and provide the entity data that way. And so was born the parser idea.
The syntax is:
"entity": {
"parser": {
"file": "path to the Javascript file containing the parser method",
"data": "path to the file containing the data. An SQL file? A mongoose file? Etc. etc."
}
}
Remember that you can always provide the paths as raw strings or use h.getArg
or h.askUser
methods to get the path as the command line argument or ask from the command line, respectively. The path is relative to the scenario file.
The parser file
that is being asked for must have the following syntax:
module.exports = function(content) {
var output = {};
// do something with content and assign to output
return output;
}
An example of a parser that attaches a magic
key to any json
file that doesn't have one:
module.exports = function(content) {
var output = JSON.parse(content);
output.magic = "magic";
return output;
};
Basically what happens in here is that mtml
will pass the file provided in the scenario file's entity.parser.data
key into the parser function as the content
argument. Then mtml
will assign the returned object into the e
convenience variable that can be directly used in the scenario file and the template files.
Licensed under MIT
Copyright (c) 2018 Damian Chrzanowski pjdamian.chrzanowski@gmail.com, MIT License
voca, Copyright (c) 2016 Dmitri Pavlutin, MIT License
chalk, Copyright (c) Sindre Sorhus sindresorhus@gmail.com (sindresorhus.com), MIT License
mkdirp, Copyright 2010 James Halliday (mail@substack.net), MIT License
lodash, Copyright JS Foundation and other contributors https://js.foundation/, MIT License
readline-sync, Copyright (c) 2018 anseki, MIT License
ejs, copyright 2012 mde@fleegix.org, Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)