Skip to content

Commit

Permalink
Feature/112 support record template (KreativJos#120)
Browse files Browse the repository at this point in the history
* Added base record template

* Namespace file scoped conversion moved to template

* Moved useFileScopedNamespace configuration to TemplateConfiguration

* Limit Record creation when target framework is an older one

---------

Co-authored-by: Sante Barbuto <[email protected]>
  • Loading branch information
bard83 and Sante Barbuto authored May 24, 2023
1 parent a860f05 commit fe5f5e9
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 95 deletions.
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
"command": "csharpextensions.createStruct",
"title": "Struct"
},
{
"command": "csharpextensions.createRecord",
"title": "Record"
},
{
"command": "csharpextensions.createController",
"title": "Controller"
Expand Down Expand Up @@ -122,6 +126,10 @@
"group": "00_basics@3",
"command": "csharpextensions.createStruct"
},
{
"group": "00_basics@4",
"command": "csharpextensions.createRecord"
},
{
"group": "10_mvc@0",
"command": "csharpextensions.createController"
Expand Down
1 change: 1 addition & 0 deletions src/commandMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function createExtensionMappings(): Map<string, CommandMapping> {
mapping.set('Interface', getCommandMapping('createInterface', [TemplateType.Inteface]));
mapping.set('Enum', getCommandMapping('createEnum', [TemplateType.Enum]));
mapping.set('Struct', getCommandMapping('createStruct', [TemplateType.Struct]));
mapping.set('Record', getCommandMapping('createRecord', [TemplateType.Record]));
mapping.set('Controller', getCommandMapping('createController', [TemplateType.Controller]));
mapping.set('ApiController', getCommandMapping('createApiController', [TemplateType.ApiController]));
mapping.set('Razor_Page', getCommandMapping('createRazorPage', [TemplateType.RazorPageClass, TemplateType.RazorPageTemplate]));
Expand Down
2 changes: 1 addition & 1 deletion src/common/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ export default class Result<T> {
}

public static ok<T>(value: T): Result<T> { return new Result(value, Status.success(), undefined); }
public static error<T>(innerStatus ='error', info: string | undefined): Result<T> { return new Result<T>(undefined, Status.error(innerStatus), info); }
public static error<T>(innerStatus: string, info: string | undefined): Result<T> { return new Result<T>(undefined, Status.error(innerStatus), info); }
}
12 changes: 3 additions & 9 deletions src/creator/cShaprFileCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ExtensionError } from '../util';
import Template from '../template/template';
import FileHandler from '../io/fileHandler';
import NamespaceDetector from '../namespaceDetector';
import fileScopedNamespaceConverter from '../fileScopedNamespaceConverter';
import TemplateConfiguration from '../template/templateConfiguration';
import Result from '../common/result';
import statuses from './fileCreatorStatus';
Expand Down Expand Up @@ -40,16 +39,11 @@ export default class CSharpFileCreator {
return Result.error<CreatedFile>(statuses.readingTemplateError, error.toString());
}

const template = new Template(this._template, templateContent, fileScopedNamespaceConverter, this._templateConfiguration);
const template = new Template(this._template, templateContent, this._templateConfiguration);
const namespaceDetector = new NamespaceDetector(pathWithoutExtension);
const namespace = await namespaceDetector.getNamespace();

let useFileScopedNamespace = false;
if (Template.getExtension(template.getType()).endsWith('.cs')) {
useFileScopedNamespace = await fileScopedNamespaceConverter.shouldUseFileScopedNamespace(destinationFilePath);
}

const fileContent = template.build(newFilename, namespace, useFileScopedNamespace);
const fileContent = template.build(newFilename, namespace);
try {
await FileHandler.write(destinationFilePath, fileContent);
} catch (e) {
Expand All @@ -58,7 +52,7 @@ export default class CSharpFileCreator {
return Result.error<CreatedFile>(statuses.writingFileError, error.toString());
}

const cursorPositionArray = template.findCursorInTemplate(newFilename, namespace, useFileScopedNamespace);
const cursorPositionArray = template.findCursorInTemplate(newFilename, namespace);

return Result.ok<CreatedFile>({ filePath: destinationFilePath, cursorPositionArray: cursorPositionArray });
}
Expand Down
9 changes: 7 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CSharpFileCreator from './creator/cShaprFileCreator';
import Maybe from './common/maybe';
import { CommandMapping, createExtensionMappings } from './commandMapping';
import TemplateConfiguration from './template/templateConfiguration';
import CsprojReader from './project/csprojReader';

const EXTENSION_NAME = 'csharpextensions';

Expand Down Expand Up @@ -94,10 +95,14 @@ export class Extension {
const configuration = vscode.workspace.getConfiguration();
const eol = configuration.get('file.eol', EOL);
const includeNamespaces = configuration.get(`${EXTENSION_NAME}.includeNamespaces`, true);
const useFileScopedNamespace = configuration.get<boolean>('csharpextensions.useFileScopedNamespace', false);
const csprojReader = await CsprojReader.createFromPath(`${pathWithoutExtension}.cs`);
const isTargetFrameworkAboveEqualNet6 = !!csprojReader && await csprojReader.isTargetFrameworkHigherThanOrEqualToDotNet6() === true;

const createdFilesResult = await Promise.all(templates.map(async template => {
return CSharpFileCreator.create(TemplateConfiguration.create(template, eol, includeNamespaces))
.AndThen(async creator => await creator.create(templatesPath, pathWithoutExtension, newFilename));
return TemplateConfiguration.create(template, eol, includeNamespaces, useFileScopedNamespace, isTargetFrameworkAboveEqualNet6)
.AndThen(config => CSharpFileCreator.create(config)
.AndThen(async creator => await creator.create(templatesPath, pathWithoutExtension, newFilename)));
}));

if (createdFilesResult.some(result => result.isErr())) {
Expand Down
54 changes: 43 additions & 11 deletions src/template/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@ import { sortBy, uniq } from 'lodash';
import * as path from 'path';

import { TemplateType } from './templateType';
import { FileScopedNamespaceConverter } from '../fileScopedNamespaceConverter';
import TemplateConfiguration from './templateConfiguration';

export default class Template {
private static readonly ClassnameRegex = new RegExp(/\${classname}/, 'g');
private static readonly NamespaceRegex = new RegExp(/\${namespace}/, 'g');
private static readonly EolRegex = new RegExp(/\r?\n/g);
private static readonly NamespaceRegexForScoped = new RegExp(/(?<=\${namespace})/);
private static readonly NamespaceBracesRegex = new RegExp(/(?<=^)({|}| {4})/, 'gm');

private _name: string;
private _type: TemplateType;
private _content: string;
private _fileScopeConverter: FileScopedNamespaceConverter;
private _configuration: TemplateConfiguration;

constructor(type: TemplateType, content: string, fileScopeConverter: FileScopedNamespaceConverter, configuration: TemplateConfiguration) {
constructor(type: TemplateType, content: string, configuration: TemplateConfiguration) {
this._name = Template.RetriveName(type);
this._type = type;
this._content = content;
this._fileScopeConverter = fileScopeConverter;
this._configuration = configuration;
}

Expand All @@ -29,8 +28,8 @@ export default class Template {
public getContent(): string { return this._content; }
public getConfiguration(): TemplateConfiguration { return this._configuration; }

public findCursorInTemplate(filename: string, namespace: string, useFileScopedNamespace: boolean): number[] | null {
const content = this._partialBuild(filename, namespace, useFileScopedNamespace);
public findCursorInTemplate(filename: string, namespace: string): number[] | null {
const content = this._partialBuild(filename, namespace);
const cursorPos = content.indexOf('${cursor}');
const preCursor = content.substring(0, cursorPos);
const matchesForPreCursor = preCursor.match(/\n/gi);
Expand All @@ -43,16 +42,16 @@ export default class Template {
return [lineNum, charNum];
}

public build(filename: string, namespace: string, useFileScopedNamespace: boolean): string {
return this._partialBuild(filename, namespace, useFileScopedNamespace)
public build(filename: string, namespace: string): string {
return this._partialBuild(filename, namespace)
.replace('${cursor}', '')
.replace(Template.EolRegex, this._configuration.getEolSettings());
}

private _partialBuild(filename: string, namespace: string, useFileScopedNamespace: boolean) {
private _partialBuild(filename: string, namespace: string) {
let content = this._content;
if (useFileScopedNamespace) {
content = this._fileScopeConverter.getFileScopedNamespaceFormOfTemplate(this._content);
if (this._configuration.getUseFileScopedNamespace()) {
content = this._getFileScopedNamespaceFormOfTemplate(this._content);
}

content = content
Expand All @@ -63,6 +62,36 @@ export default class Template {
return content;
}

/**
* Get the file-scoped namespace form of the template.
*
* From:
* ```csharp
* namespace ${namespace}
* {
* // Template content
* // Template content
* }
* ```
*
* To:
* ```csharp
* namespace ${namespace};
*
* // Template content
* // Template content
* ```
*
* @param template The content of the C# template file.
*/
private _getFileScopedNamespaceFormOfTemplate(template: string): string {
const result = template
.replace(Template.NamespaceBracesRegex, '')
.replace(Template.NamespaceRegexForScoped, ';');

return result;
}

private _handleUsings(): string {
const includeNamespaces = this._configuration.getIncludeNamespaces();
const eol = this._configuration.getEolSettings();
Expand All @@ -86,6 +115,7 @@ export default class Template {
case TemplateType.Inteface:
case TemplateType.Enum:
case TemplateType.Struct:
case TemplateType.Record:
case TemplateType.Controller:
case TemplateType.ApiController:
case TemplateType.MsTest:
Expand Down Expand Up @@ -120,6 +150,8 @@ export default class Template {
return 'enum';
case TemplateType.Struct:
return 'struct';
case TemplateType.Record:
return 'record';
case TemplateType.Controller:
return 'controller';
case TemplateType.ApiController:
Expand Down
22 changes: 19 additions & 3 deletions src/template/templateConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,49 @@ import { EOL } from 'os';

import { ExtensionError } from '../util';
import { TemplateType } from './templateType';
import Template from './template';
import Result from '../common/result';
import templateConfigurationStatuses from './templateConfigurationStatuses';

export default class TemplateConfiguration {
private _templateType: TemplateType;
private _includeNamespaces: boolean;
private _useFileScopedNamespace: boolean;
private _eolSettings: string;
private _requiredUsings: Array<string>;
private _optionalUsings: Array<string>;

private constructor(templateType: TemplateType, includeNamespaces: boolean, eolSettings: string, requiredUsings: Array<string>, optionalUsings: Array<string>) {
private constructor(templateType: TemplateType, includeNamespaces: boolean, useFileScopedNamespace: boolean, eolSettings: string, requiredUsings: Array<string>, optionalUsings: Array<string>) {
this._templateType = templateType;
this._includeNamespaces = includeNamespaces;
this._useFileScopedNamespace = useFileScopedNamespace;
this._eolSettings = eolSettings;
this._requiredUsings = requiredUsings;
this._optionalUsings = optionalUsings;
}

public getTemplateType(): TemplateType { return this._templateType; }
public getIncludeNamespaces(): boolean { return this._includeNamespaces; }
public getUseFileScopedNamespace(): boolean { return this._useFileScopedNamespace; }
public getEolSettings(): string { return this._eolSettings; }
public getRequiredUsings(): Array<string> { return this._requiredUsings; }
public getOptionalUsings(): Array<string> { return this._optionalUsings; }

public static create(type: TemplateType, eol: string, includeNamespaces: boolean): TemplateConfiguration {
public static create(type: TemplateType, eol: string, includeNamespaces: boolean, useFileScopedNamespace = false, isTargetFrameworkAboveNet6: boolean): Result<TemplateConfiguration> {
if (type === TemplateType.Record && !isTargetFrameworkAboveNet6) {
Result.error<TemplateConfiguration>(templateConfigurationStatuses.templateConfigurationCreationError, 'The target .NET framework does not support Record');
}

const eolSettings = TemplateConfiguration.getEolSetting(eol);
let canUseFileScopedNamespace = false;
if (Template.getExtension(type).endsWith('.cs') && useFileScopedNamespace && isTargetFrameworkAboveNet6) {
canUseFileScopedNamespace = true;
}

const requiredUsings = TemplateConfiguration.retrieveRequiredUsings(type);
const optionalUsings = TemplateConfiguration.retrieveOptionalUsings(type);

return new TemplateConfiguration(type, includeNamespaces, eolSettings, requiredUsings, optionalUsings);
return Result.ok<TemplateConfiguration>(new TemplateConfiguration(type, includeNamespaces, canUseFileScopedNamespace, eolSettings, requiredUsings, optionalUsings));
}

private static getEolSetting(eol: string): string {
Expand All @@ -50,6 +64,7 @@ export default class TemplateConfiguration {
case TemplateType.Inteface:
case TemplateType.Enum:
case TemplateType.Struct:
case TemplateType.Record:
return [];
case TemplateType.Controller:
return [
Expand Down Expand Up @@ -91,6 +106,7 @@ export default class TemplateConfiguration {
case TemplateType.Inteface:
case TemplateType.Enum:
case TemplateType.Struct:
case TemplateType.Record:
case TemplateType.Controller:
case TemplateType.ApiController:
case TemplateType.MsTest:
Expand Down
5 changes: 5 additions & 0 deletions src/template/templateConfigurationStatuses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const templateConfigurationStatuses = {
templateConfigurationCreationError: 'TemplateConfigurationCreationError',
};

export default templateConfigurationStatuses;
1 change: 1 addition & 0 deletions src/template/templateType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum TemplateType {
Inteface,
Enum,
Struct,
Record,
Controller,
ApiController,
MsTest,
Expand Down
7 changes: 7 additions & 0 deletions templates/record.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
${namespaces}namespace ${namespace}
{
public record ${classname}
(
${cursor}
);
}
8 changes: 4 additions & 4 deletions test/suite/unit/creator/cSharpFileCreator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ suite('CSharpFileCreator', () => {
const error = `File already exists: ${destinationFilePath}`;


const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, useFileScopedNamespace);
const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, useFileScopedNamespace, true, true).value();
const fileCreator = CSharpFileCreator.create(configuration).value();

const result = await fileCreator.create(templatesPath, pathWithoutExtension, newFilename);
Expand All @@ -61,7 +61,7 @@ suite('CSharpFileCreator', () => {
sinon.replace(FileHandler, 'fileExists', fakeFileHandler.fileExists);
sinon.replace(FileHandler, 'read', fakeFileHandler.read);
sinon.replace(FileHandler, 'write', fakeFileHandler.write);
const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, false);
const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, false, true, true).value();
const fileCreator = CSharpFileCreator.create(configuration).value();

const result = await fileCreator.create(templatesPath, pathWithoutExtension, newFilename);
Expand All @@ -77,7 +77,7 @@ suite('CSharpFileCreator', () => {
sinon.replace(FileHandler, 'fileExists', fakeFileHandler.fileExists);
sinon.replace(FileHandler, 'read', fakeFileHandler.read);
sinon.replace(FileHandler, 'write', fakeFileHandler.write);
const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, false);
const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, false, true, true).value();
const fileCreator = CSharpFileCreator.create(configuration).value();

const result = await fileCreator.create(templatesPath, pathWithoutExtension, newFilename);
Expand All @@ -92,7 +92,7 @@ suite('CSharpFileCreator', () => {
sinon.replace(FileHandler, 'fileExists', fakeFileHandler.fileExists);
sinon.replace(FileHandler, 'read', fakeFileHandler.read);
sinon.replace(FileHandler, 'write', fakeFileHandler.write);
const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, false);
const configuration = TemplateConfiguration.create(TemplateType.Class, EOL, false, true, true).value();
const fileCreator = CSharpFileCreator.create(configuration).value();

const result = await fileCreator.create(templatesPath, pathWithoutExtension, newFilename);
Expand Down
Loading

0 comments on commit fe5f5e9

Please sign in to comment.