Skip to content

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
}