diff --git a/src/fs/lexer.test.ts b/src/fs/lexer.test.ts new file mode 100644 index 00000000..33f4fd8e --- /dev/null +++ b/src/fs/lexer.test.ts @@ -0,0 +1,25 @@ +import { lex } from './lexer'; +import { Identifier } from '../shared/tokens'; +import { ThenI, SaveThe, IntoTheFile } from './tokens'; + +test('that lexing works', async () => { + // Given I have a contract with filesystem statements in it + const contract = `Rule unknown ignore +Given I have a 'string' named 'stringToWrite' +and I have a 'string' named 'nameOfTheFile' + +Then I save the 'stringToWrite' into the file 'nameOfTheFile' +`; + // When I lex it + const lexed = await lex(contract, { data: { stringToWrite: 'foo', nameOfTheFile: 'bar' } }); + // Then the result must have no errors + expect(lexed.errors).toHaveLength(0); + // and it must have 5 tokens + expect(lexed.tokens).toHaveLength(5); + // and those tokens must be these: + expect(lexed.tokens[0].tokenType).toStrictEqual(ThenI); + expect(lexed.tokens[1].tokenType).toStrictEqual(SaveThe); + expect(lexed.tokens[2].tokenType).toStrictEqual(Identifier); + expect(lexed.tokens[3].tokenType).toStrictEqual(IntoTheFile); + expect(lexed.tokens[4].tokenType).toStrictEqual(Identifier); +}); diff --git a/src/fs/lexer.ts b/src/fs/lexer.ts new file mode 100644 index 00000000..6839ccf7 --- /dev/null +++ b/src/fs/lexer.ts @@ -0,0 +1,18 @@ +import { ZenroomParams } from '../shared/zenroom'; +import { vocab } from './tokens'; +import { getIgnoredStatements } from '../ignored'; + +import { Lexer } from 'chevrotain'; + +const FsLexer = new Lexer(vocab); + +/** + * Lexes the input text for filesystem statements. + * + * @param contract the zencode contract to be lexed. + * @returns the lexed result. + */ +export const lex = async (contract: string, params?: ZenroomParams) => { + const ignored = await getIgnoredStatements(contract, params); + return FsLexer.tokenize(ignored.join('\n')); +}; diff --git a/src/fs/parser.ts b/src/fs/parser.ts new file mode 100644 index 00000000..456330d7 --- /dev/null +++ b/src/fs/parser.ts @@ -0,0 +1,38 @@ +import { Identifier } from '../shared/tokens'; +import { ZenroomParams } from '../shared/zenroom'; +import { IntoTheFile, SaveThe, ThenI, vocab } from './tokens'; +import { lex } from './lexer'; + +import { CstParser, IToken } from 'chevrotain'; + +export type FileOverrideStatementCtx = { + ThenI: [IToken]; + SaveThe: [IToken]; + content: [IToken]; + IntoTheFile: [IToken]; + filename: [IToken]; +}; + +class Parser extends CstParser { + constructor() { + super(vocab); + this.performSelfAnalysis(); + } + + public fileOverrideStatement = this.RULE('fileOverrideStatement', () => { + this.CONSUME(ThenI); + this.CONSUME(SaveThe); + this.CONSUME(Identifier, { LABEL: 'content' }); + this.CONSUME(IntoTheFile); + this.CONSUME2(Identifier, { LABEL: 'filename' }); + }); +} + +export const FsParser = new Parser(); +export const BaseFsVisitor = FsParser.getBaseCstVisitorConstructor(); + +export const parse = async (contract: string, params?: ZenroomParams) => { + const lexed = await lex(contract, params); + FsParser.input = lexed.tokens; + return FsParser.fileOverrideStatement(); +}; diff --git a/src/fs/tokens.ts b/src/fs/tokens.ts new file mode 100644 index 00000000..5bce1151 --- /dev/null +++ b/src/fs/tokens.ts @@ -0,0 +1,34 @@ +import { Whitespace, Comment, Identifier } from '../shared/tokens'; + +import { createToken } from 'chevrotain'; + +// TODO: maybe this should be put in shared and/or the "I" part should be split up. +/** + * The "Then I" statement. + */ +export const ThenI = createToken({ + name: 'ThenI', + pattern: /Then I/ +}); + +/** + * The "save the" statement, used to write files to the filesystems. + */ +export const SaveThe = createToken({ + name: 'SaveThe', + pattern: /save the/ +}); + +/** + * The "into the file" statement, used to indicate the file to which to + * write. + */ +export const IntoTheFile = createToken({ + name: 'IntoTheFile', + pattern: /into the file/ +}); + +/** + * Vocabulary to perform filesystems actions. + */ +export const vocab = [Whitespace, Comment, ThenI, SaveThe, IntoTheFile, Identifier]; diff --git a/src/fs/visitor.test.ts b/src/fs/visitor.test.ts new file mode 100644 index 00000000..8e887673 --- /dev/null +++ b/src/fs/visitor.test.ts @@ -0,0 +1,16 @@ +import { visit } from './visitor'; + +test('that', async () => { + // Given I have a contract with filesystem statements in it + const contract = `Rule unknown ignore +Given I have a 'string' named 'stringToWrite' +and I have a 'string' named 'nameOfTheFile' + +Then I save the 'stringToWrite' into the file 'nameOfTheFile' +`; + const ast = await visit(contract, { + data: { stringToWrite: 'hello world', nameOfTheFile: 'hello-world.txt' } + }); + + expect(true).toStrictEqual(true); +}); diff --git a/src/fs/visitor.ts b/src/fs/visitor.ts new file mode 100644 index 00000000..2ad39f05 --- /dev/null +++ b/src/fs/visitor.ts @@ -0,0 +1,36 @@ +import { ZenroomParams } from '../shared/zenroom'; +import { BaseFsVisitor, parse, type FileOverrideStatementCtx } from './parser'; + +import { type CstNode } from 'chevrotain'; + +export type FileOverrideStatement = { + content: string; + filename: string; +}; + +interface Visitor { + visit(params: CstNode): FileOverrideStatement; +} + +class Visitor extends BaseFsVisitor { + constructor() { + super(); + this.validateVisitor(); + } + + fileOverrideStatement(ctx: FileOverrideStatementCtx) { + const content = ctx.content[0].image; + const filename = ctx.filename[0].image; + return { + content: content, + filename: filename + }; + } +} + +export const FsVisitor = new Visitor(); + +export const visit = async (contract: string, params?: ZenroomParams) => { + const parsed = await parse(contract, params); + return FsVisitor.visit(parsed); +};