Mixing in custom code

Adding your own code to generated code is risky, because when you re-generate that code, your manual changes will be overwritten. Therefore, if you want to avoid this, you should always keep your generated code and custom code separated. There are many different (opinionated) approaches to achieve this separation, and the approach you choose depends on your software design and personal- or team preferences. This section provides a few suggestions to get you started:

Your target programming language might also have a way to blend code from different files. For C#, for example, you can generate partial classes and methods to split a class, struct or interface method over two or more sources. For TypeScript, there is a feature request for partial classes, which we hope will make it into a future release.

1. Separate concerns

Say that you would like to generate a class that contains both attributes (properties) and operations (methods, functions). Generating attributes is usually easy. Generating operations is often hard, or just impossible because their implementation is too specific to be model-driven. However, generating just the signature of an operation is easier. For example, consider the following snippet that uses @yellicode/csharp to generate C# methods (where eachClass is a class in a Yellicode model):

eachClass.ownedOperations.forEach(op => {
    csharp.writeMethodBlock(op, () => {
	   csharp.writeLine('throw new NotImplementedException("Sorry, I need to write my own implementation.")');
    });                        
});

A good way to deal with this can be to delegate the actual implementation of the method to a separate class. While it doesn't keep the implementation nicely encapsulated in a single class, it does have the advantage of separating logic from 'boilerplate' code. Let's adjust the snippet above to delegate the implementation to another class:

eachClass.ownedOperations.forEach(op => {
    csharp.writeMethodBlock(op, () => {
        const writeReturn = op.getReturnParameter() !== null;
        const inputParameters = op.getInputParameters().map(p => p.name); 
        inputParameters.unshift('this'); // pass a reference to ourselves
        if (writeReturn) {
            csharp.writeLine(`return ${cls.name}Delegate.${op.name}(${inputParameters.join(', ')});`);
        }
        else csharp.writeLine(`${cls.name}Delegate.${op.name}(${inputParameters.join(', ')});`);
    });                        
});

The resulting C# code (a fictional example of a ToString() method would then look as follows:

// Generated code
public class MyClass
{
    public decimal MyProperty { get; set; }

    public string ToString(string format)
    {
        return MyClassDelegate.ToString(this, format);
    }
}

// Custom code
internal class MyClassDelegate
{
    public static ToString(MyClass instance, string format)
    {
        return instance.MyProperty.ToString();
    }
}

Off course, this implementation assumes that the MyClassDelegate class exists elsewhere, so you should create this one manually.

2. Inheritance

Another approach is to use class inheritance. With this approach, you only generate base classes and leave the custom implementation to descendent classes. Let's update the example above to generate an abstract base class:

csharp.writeClassBlock(eachClass, () => {     
    // endure that all methods are abstract
    eachClass.ownedOperations.forEach(op => {                       
        csharp.writeMethodDeclaration(op, { isAbstract: true });                        
    });
}, { isAbstract: true }); // ensure that the class is abstract         

The resulting C# code would then look as follows:

// Generated code
public abstract class MyClass
{
    public decimal MyProperty { get; set; }

    public abstract string ToString(string format);    
}

// Custom code
public class MyClassImplementation : MyClass
{
    public override ToString(string format)
    {
        return this.MyProperty.ToString();
    }
}    

This approach provides better encapsulation than the first one. However, it does break one popular principle in Object Oriented Programming, namely to favor composition over inheritance.

3. Mix in custom regions

If you don't want your code to be separated at all, Yellicode allows you to write you custom code in one or more separate files and merge designated regions from these files with the generated code.

Say we want to generate a TypeScript class named Person having a firstName and a lastName property. For simplicity, we will not create a model but hard-code the class as follows:

import { TextWriter } from '@yellicode/core';  
import { Generator } from '@yellicode/templating';  

Generator.generate({ outputFile: './person.ts' },
    (writer: TextWriter) => {
        writer.writeLine('export class Person {');
        writer.increaseIndent();
        writer.writeLine('public firstName: string;')
        writer.writeLine('public lastName: string;')
        writer.decreaseIndent();
        writer.writeLine('}');
    }
);

But, what if we wanted to add a function that returns the person's full name? This is not the kind of code that you would typically generate, so let's create a separate file named custom-code.partial.template.ts for this:

export class Person {
    /// <person-functions>
    public getFullName() {
        return `${this.firstName} ${this.lastName}`;
    }
    /// </person-functions>
}

Note the region markers in the file. These markers let our template locate the regions to be pulled in. To do so, extend the template as follows, adding a single call to writeFileRegion:

import { TextWriter } from '@yellicode/core';  
import { Generator } from '@yellicode/templating';  

Generator.generate({ outputFile: 'person.ts' },
    (writer: TextWriter) => {
        writer.writeLine('export class Person {');
        writer.increaseIndent();
        writer.writeLine('public firstName: string;')
        writer.writeLine('public lastName: string;')
        writer.writeFileRegion('person-functions', './custom-code.partial.template.ts');
        writer.decreaseIndent();
        writer.writeLine(`}`);
    }
);

If you haven't done so, start Yellicode and watch the resulting person.ts file.

Customizing the region marker format

The format of the region start- and end markers in the partial file might not work well with the programming language you are targeting. You can customize this format by creating your own implementation of the RegionMarkerFormatter interface. Say we want to use the marker format #start my-region-name ... code here ... #end my-region-name, the formatter implementation would look as follows:

import { RegionMarkerFormatter } from '@yellicode/templating';

export class CustomMarkerFormatter implements RegionMarkerFormatter {
    public getRegionStartMarker(regionName: string): string {
        return `#start ${regionName}`;
    }
    
    public getRegionEndMarker(regionName: string): string {
        return `#end ${regionName}`;
    }
}

Then, we would apply this formatter to the template as follows, assuming the custom formatter to be in a file named custom-marker-formatter.ts.

import { TextWriter } from '@yellicode/core';  
import { Generator } from '@yellicode/templating';  
import { CustomMarkerFormatter } from './custom-marker-formatter';

Generator.generate({ outputFile: 'person.ts', regionMarkerFormatter: new CustomMarkerFormatter() },
    (writer: TextWriter) => {
        // code here stays the same
    }
);

4. Using the 'once' output mode

The once mode is useful when you want to use a Yellicode template for code scaffolding. When using this mode, the output file will not be truncated if it already exists. You can configure the output mode in the codegenconfig.json as follows:
{
  "templates": [    
    {
      "templateFile": "./my-scaffolding.template.ts",      
      "outputMode": "once"
    }
  ],
  "compileTypeScript": true
}

Alternatively, you can also set the output mode inside your template. This can be useful if you need more control over when to overwrite files:

import { TextWriter } from '@yellicode/core';
import { Generator, OutputMode } from '@yellicode/templating';

// always generate the file only once (or set the outputMode based on some condition instead)
Generator.generate({ outputFile: './some-output.txt', outputMode: OutputMode.Once }, (writer: TextWriter) => {

});

// or, using generateFromModel: 
Generator.generateFromModel({ outputFile: './some-output.txt', outputMode: OutputMode.Once }, (writer: TextWriter, model: any) => {

});