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:

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
}