Skip to content

Commit

Permalink
parse expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
kla committed Oct 20, 2024
1 parent 8ecdab3 commit f35bad8
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 7 deletions.
69 changes: 69 additions & 0 deletions src/yamler/expressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const expressionRegex = /\$\{\{\s*([^}]+)\s*\}\}/

function parseToken(content: string): [string, string[]] {
const parts = content.trim().split(/\s+/)
if (parts.length === 0) return [content, []]

const [name, ...args] = parts
return [name, args]
}

/**
* Parses an array of template expression arguments, handling quoted strings.
*
* @param args - The array of arguments to parse.
* @returns An array of parsed arguments, with quoted strings combined.
*/
function parseTemplateExpressionArgs(args: string[]): string[] {
if (!Array.isArray(args)) return args

const parsedArgs: string[] = []
let currentArg = ''
let inQuotes = false

for (const arg of args) {
if (!inQuotes) {
if (arg.startsWith("'") && arg.endsWith("'")) {
parsedArgs.push(arg.slice(1, -1).trim())
} else if (arg.startsWith("'")) {
inQuotes = true
currentArg = arg.slice(1)
} else {
parsedArgs.push(arg.trim())
}
} else {
if (arg.endsWith("'")) {
inQuotes = false
currentArg += ' ' + arg.slice(0, -1)
parsedArgs.push(currentArg.trim())
currentArg = ''
} else {
currentArg += ' ' + arg
}
}
}

if (currentArg) {
parsedArgs.push(currentArg.trim())
}

return parsedArgs
}

/**
* Parses a template expression and returns the function name and arguments.
*
* @param expression - The template expression to parse.
* @returns An object containing the function name and parsed arguments or undefined if the expression is invalid.
*/
export function parseTemplateExpression(expression: string): { funcName: string; args: string[] } | undefined {
const [content] = expression.match(expressionRegex) || []

if (!content)
return undefined

const [funcName, argString] = parseToken(content.slice(3, -2).trim())
const args = parseTemplateExpressionArgs(argString)

return { funcName, args }
}
35 changes: 30 additions & 5 deletions src/yamler/yamler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { dumpOptions, importRegex, extendsRegex, rootExtendsRegex, anchorNamePrefix } from './index'
import { deepRemoveKeys, dig, exit, resolve } from '~/utils'
import { deepRemoveKeys, dig, exit, resolve, deepMapWithKeys } from '~/utils'
import { parseTemplateExpression } from './expressions'
import * as fs from 'fs'
import * as path from 'path'
import yaml from 'js-yaml'
import icons from '~/icons'
import Import from './import'

export function loadFile(filePath: string): Record<string, any> {
return new YamlER(filePath).load()
export function loadFile(filePath: string, expressionCallback?: Function | undefined): Record<string, any> {
return new YamlER(filePath, { expressionCallback }).load()
}

export function dump(obj: any): string {
Expand All @@ -20,10 +21,12 @@ export default class YamlER {
public imports: Record<string, Import>
public content: string
public attributes: Record<string, any>
private expressionCallback: Function | undefined

constructor(filePath: string, options: { parentFile?: string } = {}) {
constructor(filePath: string, options: { parentFile?: string, expressionCallback?: Function | undefined } = {}) {
this.filePath = resolve(path.dirname(options.parentFile || filePath), filePath)
this.parentFile = options.parentFile
this.expressionCallback = options.expressionCallback
}

get baseDir(): string {
Expand All @@ -35,7 +38,14 @@ export default class YamlER {
this.parseImports()
this.parseExtends()
this.parseResolveRelative()
return this.attributes = deepRemoveKeys(yaml.load(this.content), [ new RegExp(`^${anchorNamePrefix}`) ])
this.attributes = yaml.load(this.content)
this.attributes = deepRemoveKeys(this.attributes, [ new RegExp(`^${anchorNamePrefix}`) ])

// only parse expressions on the final set of attributes
if (!this.parentFile)
this.parseAllExpressions()

return this.attributes
}

load(): Record<string, any> {
Expand Down Expand Up @@ -96,6 +106,21 @@ export default class YamlER {
this.content = Array.from(prepends).join('\n\n') + '\n\n' + this.content
}

private parseExpression(path: string, obj: string | undefined | null) {
if (obj && typeof(obj) === 'string') {
const expression = parseTemplateExpression(obj)

if (expression && this.expressionCallback) {
obj = this.expressionCallback(path, expression.funcName, expression.args)
}
}
return obj
}

private parseAllExpressions() {
this.attributes = deepMapWithKeys(this.attributes, (path, key, value) => [ this.parseExpression(path, key), this.parseExpression(path, value) ])
}

private findImport(name: string): Import {
const importName = name.split('.')[0]
const imp = this.imports[importName] || exit(1, { message: `${icons.error} Couldn't find import for '${importName}' referenced in '${this.filePath}'` })
Expand Down
9 changes: 7 additions & 2 deletions tests/unit/yamler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ describe('YamlER', () => {
const composeYaml = resolve(fixturesDir, 'some_service.staxfile')
let yaml

beforeEach(() => yaml = loadFile(composeYaml))
const expressionCallback = (path, key, value) => {
return '<' + [key].concat(value).join(' ') + '>'
}

beforeEach(() => yaml = loadFile(composeYaml, expressionCallback))

it('loads and processes a YAML file with imports', () => {
console.log(dump(yaml))
expect(yaml.stax.app).toBe('some_service')
expect(Object.keys(yaml)).toEqual(['stax', 'volumes', 'services'])
})

it('can extend at the root', () => {
expect(Object.keys(yaml.volumes)).toEqual(['shared-home', '${{ stax.workspace_volume }}'])
expect(Object.keys(yaml.volumes)).toEqual(['shared-home', '<stax.workspace_volume>'])
expect(Object.keys(yaml.services)).toEqual(['web'])
expect(yaml.services.web).toBeDefined()
})
Expand Down

0 comments on commit f35bad8

Please sign in to comment.