diff --git a/packages/core/js/data_generation/inline-schema-rule.js b/packages/core/js/data_generation/inline-schema-rule.js new file mode 100644 index 00000000..820756c0 --- /dev/null +++ b/packages/core/js/data_generation/inline-schema-rule.js @@ -0,0 +1,33 @@ +function startsConstraint(trimmedLine) { + return /^IF\s+(?:\[|\(|NOT\b)/i.test(trimmedLine); +} + +function looksLikeInlineRuleSpec(ruleText) { + const trimmed = String(ruleText ?? '').trim(); + if (trimmed.length === 0 || startsConstraint(trimmed)) { + return false; + } + + if ( + /^(?:enum|literal|regex|datatype\.(?:enum|literal|regex)|awd\.datatype\.(?:enum|literal|regex))\s*\(/i.test(trimmed) + ) { + return true; + } + + if (/^(?:faker\.)?[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+(?:\s*\(.*\)\s*|\s*)$/i.test(trimmed)) { + return true; + } + + if (!trimmed.includes(',')) { + return false; + } + + const values = trimmed.split(',').map((value) => value.trim()); + if (values.length < 2 || values.some((value) => value.length === 0 || value.length > 50)) { + return false; + } + + return !values.some((value) => /[[\]{}()^$*+?|\\]/.test(value) || (value.includes('.') && /[A-Z]/.test(value))); +} + +export { startsConstraint, looksLikeInlineRuleSpec }; diff --git a/packages/core/js/data_generation/rulesParser.js b/packages/core/js/data_generation/rulesParser.js index 5768dd88..f1436525 100644 --- a/packages/core/js/data_generation/rulesParser.js +++ b/packages/core/js/data_generation/rulesParser.js @@ -1,38 +1,7 @@ import { TestDataRules } from './testDataRules.js'; import { SchemaParsingErrors } from './schema-parsing-errors.js'; import { parseConstraintText } from './schema-constraint-parser.js'; - -function startsConstraint(trimmedLine) { - return /^IF\s+(?:\[|\(|NOT\b)/i.test(trimmedLine); -} - -function looksLikeInlineRuleSpec(ruleText) { - const trimmed = String(ruleText ?? '').trim(); - if (trimmed.length === 0 || startsConstraint(trimmed)) { - return false; - } - - if ( - /^(?:enum|literal|regex|datatype\.(?:enum|literal|regex)|awd\.datatype\.(?:enum|literal|regex))\s*\(/i.test(trimmed) - ) { - return true; - } - - if (/^(?:faker\.)?[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+(?:\s*\(.*\)\s*|\s*)$/i.test(trimmed)) { - return true; - } - - if (!trimmed.includes(',')) { - return false; - } - - const values = trimmed.split(',').map((value) => value.trim()); - if (values.length < 2 || values.some((value) => value.length === 0 || value.length > 50)) { - return false; - } - - return !values.some((value) => /[[\]{}()^$*+?|\\]/.test(value) || (value.includes('.') && /[A-Z]/.test(value))); -} +import { looksLikeInlineRuleSpec, startsConstraint } from './inline-schema-rule.js'; function parseInlineRuleDefinition(line) { const source = String(line ?? ''); @@ -42,15 +11,18 @@ function parseInlineRuleDefinition(line) { } const name = source.slice(0, index).trim(); - const rule = source.slice(index + 1).trim(); + const rawRulePortion = source.slice(index + 1); + const rule = rawRulePortion.trim(); if (name.length === 0 || !looksLikeInlineRuleSpec(rule)) { continue; } + const leadingWhitespaceLength = rawRulePortion.length - rawRulePortion.trimStart().length; + return { name, rule, - separator: ': ', + separator: `:${rawRulePortion.slice(0, leadingWhitespaceLength)}`, }; } diff --git a/packages/core/src/index.js b/packages/core/src/index.js index fc2a9693..7547f98d 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -11,6 +11,7 @@ import { CombinationAlgorithm, CombinationsTestDataGenerator, } from '../js/data_generation/n-wise/combinationsTestDataGenerator.js'; +import { looksLikeInlineRuleSpec, startsConstraint } from '../js/data_generation/inline-schema-rule.js'; import { parseSchemaText } from '../js/data_generation/schema-conversion.js'; import { hasSafeFakerLiteralArguments } from '../js/data_generation/faker/safeLiteralArgumentParser.js'; import { @@ -78,34 +79,6 @@ const SUPPORTED_FORMATS = [ 'asciitable', ]; -function looksLikeInlineSchemaRule(ruleText) { - const trimmed = String(ruleText ?? '').trim(); - if (trimmed.length === 0 || /^IF\s+(?:\[|\(|NOT\b)/i.test(trimmed)) { - return false; - } - - if ( - /^(?:enum|literal|regex|datatype\.(?:enum|literal|regex)|awd\.datatype\.(?:enum|literal|regex))\s*\(/i.test(trimmed) - ) { - return true; - } - - if (/^(?:faker\.)?[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+(?:\s*\(.*\)\s*|\s*)$/i.test(trimmed)) { - return true; - } - - if (!trimmed.includes(',')) { - return false; - } - - const values = trimmed.split(',').map((value) => value.trim()); - if (values.length < 2 || values.some((value) => value.length === 0 || value.length > 50)) { - return false; - } - - return !values.some((value) => /[[\]{}()^$*+?|\\]/.test(value) || (value.includes('.') && /[A-Z]/.test(value))); -} - function extractRuleLines(textSpec) { if (typeof textSpec !== 'string') { return []; @@ -115,7 +88,7 @@ function extractRuleLines(textSpec) { let pendingName = null; for (const line of lines) { const trimmed = line.trim(); - if (trimmed.length === 0 || /^\s*#/.test(line) || /^IF\s+(?:\[|\(|NOT\b)/i.test(trimmed)) { + if (trimmed.length === 0 || /^\s*#/.test(line) || startsConstraint(trimmed)) { pendingName = null; continue; } @@ -126,7 +99,7 @@ function extractRuleLines(textSpec) { continue; } const rule = line.slice(separatorIndex + 1).trim(); - if (looksLikeInlineSchemaRule(rule)) { + if (looksLikeInlineRuleSpec(rule)) { ruleLines.push(rule); pendingName = null; matchedInlineRule = true; diff --git a/packages/core/src/tests/data_generation/unit/rulesParser.test.js b/packages/core/src/tests/data_generation/unit/rulesParser.test.js index a6039819..519525a3 100644 --- a/packages/core/src/tests/data_generation/unit/rulesParser.test.js +++ b/packages/core/src/tests/data_generation/unit/rulesParser.test.js @@ -140,6 +140,20 @@ Status: person.jobTitle`; expect(parser.renderSpecFromRulesWithTokens(parser.testDataRules.rules)).toBe(inputText); }); + test('preserves authored inline separator spacing when rebuilding from parsed tokens', () => { + const inputText = `Name:person.fullName +Role: enum(admin,user)`; + + const parser = new RulesParser(faker, RandExp); + parser.parseText(inputText); + + expect(parser.getSchemaTokens()).toEqual([ + expect.objectContaining({ kind: 'rule', separator: ':' }), + expect.objectContaining({ kind: 'rule', separator: ': ' }), + ]); + expect(parser.renderSpecFromRulesWithTokens(parser.testDataRules.rules)).toBe(inputText); + }); + test('preserves comments and blank lines when rebuilding from rule comments', () => { const inputText = `# one