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:
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
}