Languages
ProJor has a basic mechanism that allows specifying domain-specific languages for the local project.
This is useful, if you want to replace the YAML
format with something that better fits your needs.
For example, consider this data collection:
yaml
id: services
name: Services
description: The services of the application.
schema: Service
objects:
- name: user-admin-service
description: Used by admin users to manage users.
operations:
- name: create_user
description: Creates a new user in the system
- name: suspend_user
description: Suspends a user in the system
- name: registration-service
description: Used by non-member (unauthenticated) users to register new accounts.
operations:
- name: register_user
description: Registers a new user in the system
This would better be described in a file, called .projor/services.servicefile
:
/// Used by admin users to manage users.
service UserAdminService {
/// Creates a new user in the system
create_user()
/// Suspends a user in the system
suspend_user()
}
/// Used by non-member (unauthenticated) users to register new accounts.
service RegistrationService {
/// Registers a new user in the system
register_user()
}
This could be achieved by creating the servicefile.plang.js
file:
See full code of the parser
javascript
/// Function to parse the contents of a `.servicefile`
/// In this case, we will return a single data collection
async function parseServicefile(filename, content) {
const parsedServices = [];
// Regular expression to match service blocks in the content
// Captures:
// - commentBlock: any preceding comments (lines starting with '///')
// - serviceName: the name of the service
// - serviceBody: the content inside the braces {}
const serviceRegex = /((?:\/\/\/[^\n]*\n)*)\s*service\s+(\w+)\s*{\s*([^}]*)}/g;
console.log(filename);
let match;
while ((match = serviceRegex.exec(content)) !== null) {
const [fullMatch, commentBlock, serviceName, serviceBody] = match;
// Extract service description from commentBlock
const serviceDescription = extractDescription(commentBlock).trim() || "No description provided.";
// Parse operations within the serviceBody
const operations = parseOperations(serviceBody);
// Create the service object
const service = {
name: serviceName,
description: serviceDescription,
operations: operations
};
parsedServices.push(service);
}
return {
id: "services",
name: "Services",
description: "The services of the application.",
schema: "Service",
objects: parsedServices
};
}
/**
* Extracts the description text from a block of comments.
* Each comment line starts with '///'.
* The description is formed by concatenating the text after '///' in each line.
*
* @param {string} commentBlock - The block of comments.
* @returns {string} - The extracted description.
*/
function extractDescription(commentBlock) {
// Split the comment block into individual lines
const lines = commentBlock.split('\n');
// Extract the text after '///' from each line
const descriptionLines = lines.map(line => {
const match = line.match(/\/\/\/\s?(.*)/);
return match ? match[1].trim() : '';
}).filter(line => line.length > 0);
// Join the lines to form the full description
return descriptionLines.join(' ');
}
/**
* Parses the operations within a service body.
* Each operation may have preceding comments.
*
* @param {string} serviceBody - The content inside the service braces.
* @returns {Array} - An array of operation objects with name and description.
*/
function parseOperations(serviceBody) {
const operations = [];
// Regular expression to match operations
// Captures:
// - commentBlock: any preceding comments (lines starting with '///')
// - operationName: the name of the operation
const operationRegex = /((?:\/\/\/[^\n]*\n)*)\s*(\w+)\s*\(\s*\)/g;
let match;
while ((match = operationRegex.exec(serviceBody)) !== null) {
const [fullMatch, commentBlock, operationName] = match;
// Extract operation description from commentBlock
const operationDescription = extractDescription(commentBlock).trim() || "No description provided.";
// Create the operation object
const operation = {
name: operationName,
description: operationDescription
};
operations.push(operation);
}
return operations;
}
/**
* Parses all `.servicefile` files in the project.
*
* Receives an array of file objects, each containing a "filename" and "content" property.
* The function ensures that only one service file is processed, as per the requirement.
* If more than one file is provided, it returns an error object.
* Otherwise, it parses the single service file using the `parseServicefile` function.
*
* @param {Array} files - Array of file objects to be parsed.
* @returns {Object} - Parsed data collection or an error object.
*/
async function parseAllServicefiles(files) {
if (files.length !== 1) {
return {
errors: [
{
filename: "<unknown>",
message: "Only one service file is allowed"
}
]
};
}
return await parseServicefile(files[0].filename, files[0].content);
}
/// `.plang.js` files must return an object
return {
extensions: ['.servicefile'], /// These files will be parsed by this language
parse: parseAllServicefiles /// The function to parse the files
}
The PLang parser generator
We have created the PLang parser generator project, which contains many .parser.js
files you can use as-is in your projects. It also allows you to generate new parsers, which will be generated by ProJor.
Currently supported languages include:
- PagesLang - Can be used to describe pages of a frontend application.
Example PagesLang code
// The admin dashboard page
dashboard Dashboard[ic:baseline-dashboard] {
title Admin dashboard
message This is the admin dashboard. It is left empty for this example.
stat widget 24H Revenue[$1,000,000] { }
post action Open Products[ic:baseline-shopping-cart] goes to Products
}
// The list of products page
list_page Products[ic:baseline-shopping-cart] {
title Products
message This is the list of products. You can edit or delete them.
// The unique identifier for each product
column id: number example 465191de-2108-4dc0-ac81-d3d53f2176e1
column name: string example Vacuum Cleaner
// The description of the product
column description: string example This device will suck the dust from the floor to make it cleaner.
// The number of buys in the last week
column buys_last_week: number example 57
// The price of the product
column price: string example $99.99
// Allow the user to navigate to the EditProduct page
item action Edit[ic:baseline-edit] goes to EditProduct
item action Delete[ic:baseline-delete] goes to Products
pre action Create[ic:baseline-add] goes to CreateProduct
post action Open Dashboard[ic:baseline-dashboard] goes to Dashboard
}
edit_page EditProduct[ic:baseline-edit]<unlisted> {
title Edit Vacuum Cleaner
message You are editing product 465191de-2108-4dc0-ac81-d3d53f2176e1 called Vacuum Cleaner.
// The name of the product
field name: string example Vacuum Cleaner
field description: string example This device will suck the dust from the floor to make it cleaner.
// The price of the product
field price: string example $99.99
// Cancel goes back to the Products page
post action Cancel[ic:baseline-cancel] goes to Products
// Save goes back to the Products page
post action Save[ic:baseline-save] goes to Products
post action Delete[ic:baseline-delete] goes to Products
}
edit_page CreateProduct[ic:baseline-add]<unlisted> {
title Create Product
message You are creating a new product.
// The name of the product
field nameOfNewProduct: string example Vacuum Cleaner
field descriptionOfNewProduct: string example This device will suck the dust from the floor to make it cleaner.
// The price of the product
field priceOfNewProduct: string example $99.99
// Cancel goes back to the Products page
post action Cancel[ic:baseline-cancel] goes to Products
// Save goes back to the Products page
post action Save[ic:baseline-save] goes to Products
}
- EntityLang - Can be used to describe entities for CRUD APIs or object-relational mapping.
Example EntityLang code
entity OrganizationUnit {
col name: String
// Employees in the org unit
reverse join employees: List<Employee>(org_unit)
}
// An employee
entity Employee {
col first_name: String (matches /^[A-Z][a-z]{1,64}$/)
col last_name: String (matches /^[A-Z][a-z]{1,64}$/)
col email: String (matches /^[a-zA-Z0-9\._%+\-]+@([a-zA-Z0-9\.\-_]+\.)+[a-zA-Z]{2,}$/)
col emp_number: Integer (unique, >= 0, <= 999999)
// Org unit of employee
join org_unit: OrganizationUnit
}
- ServicesLang - Used to describe DTOs, services and service operations to create a logical representation of an RPC (Remote Procedure Call) system.
Example ServicesLang code
dto Team {
+id: String
+name: String
+description: String
}
dto ListTeamsResponse {
+data: List<Team>
+total: Integer
}
dto Empty {
}
service TeamService {
+ListAllTeams(Empty) -> ListTeamsResponse
}
- WebServicesLang - Can be used to describe HTTP web services, their data structures, and endpoints.
Example WebServicesLang code
dto Team {
+id: String
+name: String
+description: String
}
dto NewTeam {
+name: String
+description: String
}
dto ListTeamsResponse {
+data: List<Team>
+total: Integer
}
dto Empty {
}
service TeamService(/api/v1/teams) {
GET / () -> ListTeamsResponse
POST / (NewTeam) -> Team
}