Skip to content

Commit

Permalink
Merge pull request #82 from firebase/koss-regexp
Browse files Browse the repository at this point in the history
Use unquoted RegExp in test function.
  • Loading branch information
mckoss committed Nov 20, 2015
2 parents 3e9de52 + e7cc771 commit 84901da
Show file tree
Hide file tree
Showing 12 changed files with 388 additions and 37 deletions.
3 changes: 3 additions & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changed - Support string.test(/regexp/) format (formerly required regexp in a quoted string).
feature - Added regexp test and samples.
fixed - Parser failed when empty string used.
5 changes: 5 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ gulp.task('browserify-mail-test', function() {
return browserifyToDist('lib/test/mail-test', { exclude: 'bolt' });
});

gulp.task('browserify-regexp-test', function() {
return browserifyToDist('lib/test/regexp-test', { exclude: 'bolt' });
});

gulp.task('browserify-chat-test', function() {
return browserifyToDist('lib/test/chat-test', { exclude: 'bolt' });
});
Expand All @@ -142,6 +146,7 @@ gulp.task('browserify', ['browserify-bolt',
'browserify-parser-test',
'browserify-generator-test',
'browserify-mail-test',
'browserify-regexp-test',
'browserify-chat-test',
'browserify-util-test',
'browserify-ast-test',
Expand Down
47 changes: 47 additions & 0 deletions samples/regexp.bolt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
path / {
read() = true;
write() = true;
}

path /ss is SocialSecurity;
path /integer is IntegerString;
path /float is FloatString;
path /int is Integer;
path /alpha is Alpha;
path /year is Year;
path /date is ISODate;

type SocialSecurity extends String {
validate() = this.test(/^\d\d\d-\d\d-\d\d\d\d$/);
}

// Only an integer string
type IntegerString extends String {
validate() = this.test(/^-?\d+$/);
}

// A floating point number (or integer).
type FloatString extends String {
validate() = this.test(/^-?(\d+\.?\d*|\.\d+)$/);
}

// An integral number (no fractional part)
type Integer extends Number {
validate() = (this + '').test(/^-?\d+$/);
}

// Only letters
type Alpha extends String {
validate() = this.test(/^[a-z]+$/i);
}

// YYYY is 20th or 21st century
type Year extends String {
validate() = this.test(/^(19|20)\d\d$/);
}

// YYYY-MM-DD in 20th or 21st century
// Note: Does not validate day-of-month is valid.
type ISODate extends String {
validate() = this.test(/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/);
}
27 changes: 27 additions & 0 deletions samples/regexp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"rules": {
".read": "true",
".write": "true",
"ss": {
".validate": "newData.isString() && newData.val().matches(/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/)"
},
"integer": {
".validate": "newData.isString() && newData.val().matches(/^-?\\d+$/)"
},
"float": {
".validate": "newData.isString() && newData.val().matches(/^-?(\\d+\\.?\\d*|\\.\\d+)$/)"
},
"int": {
".validate": "newData.isNumber() && (newData.val() + '').matches(/^-?\\d+$/)"
},
"alpha": {
".validate": "newData.isString() && newData.val().matches(/^[a-z]+$/i)"
},
"year": {
".validate": "newData.isString() && newData.val().matches(/^(19|20)\\d\\d$/)"
},
"date": {
".validate": "newData.isString() && newData.val().matches(/^(19|20)\\d\\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/)"
}
}
}
37 changes: 30 additions & 7 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export interface ExpValue extends Exp {
value: any;
}

export interface RegExpValue extends ExpValue {
modifiers: string;
}

export interface ExpNull extends Exp {
}

Expand Down Expand Up @@ -106,11 +110,10 @@ export interface Loggers {
error: (message: string) => void;
warn: (message: string) => void;
};
export var string: (string) => ExpValue = valueGen('String');
export var boolean: (boolean) => ExpValue = valueGen('Boolean');
export var number: (number) => ExpValue = valueGen('Number');
export var array = valueGen('Array');
export var regexp = valueGen('RegExp');
export var string: (v: string) => ExpValue = valueGen('String');
export var boolean: (v: boolean) => ExpValue = valueGen('Boolean');
export var number: (v: number) => ExpValue = valueGen('Number');
export var array: (v: Array<any>) => ExpValue = valueGen('Array');

export var neg = opGen('neg', 1);
export var not = opGen('!', 1);
Expand Down Expand Up @@ -284,6 +287,22 @@ function valueGen(typeName: string): ((val: any) => ExpValue) {
};
}

export function regexp(pattern: string, modifiers = ""): RegExpValue {
switch (modifiers) {
case "":
case "i":
break;
default:
throw new Error("Unsupported RegExp modifier: " + modifiers);
}
return {
type: 'RegExp',
valueType: 'RegExp',
value: pattern,
modifiers: modifiers
};
}

function cmpValues(v1: ExpValue, v2: ExpValue): boolean {
return v1.type === v2.type && v1.value === v2.value;
}
Expand Down Expand Up @@ -546,9 +565,13 @@ export function decodeExpression(exp: Exp, outerPrecedence?: number): string {
result = util.quoteString((<ExpValue> exp).value);
break;

// RegExp assumed to be in correct format.
// RegExp assumed to be in pre-quoted format.
case 'RegExp':
result = (<ExpValue> exp).value;
let regexp = <RegExpValue> exp;
result = '/' + regexp.value + '/';
if (regexp.modifiers !== '') {
result += regexp.modifiers;
}
break;

case 'Array':
Expand Down
20 changes: 10 additions & 10 deletions src/rules-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,16 @@ export class Generator {
replace: ast.method(['this', 's', 'r'],
ast.call(ast.reference(ast.value(thisVar), ast.string('replace')),
[ ast.value(ast.variable('s')), ast.value(ast.variable('r')) ])),
test: ast.method(['this', 's'],
test: ast.method(['this', 'r'],
ast.call(ast.reference(ast.value(thisVar), ast.string('matches')),
[ ast.call(ast.variable('@RegExp'), [ast.variable('s')]) ])),
[ ast.call(ast.variable('@RegExp'), [ast.variable('r')]) ])),
});

registerAsCall('Number', 'isNumber');
registerAsCall('Boolean', 'isBoolean');

this.symbols.registerFunction('@RegExp', ['s'],
ast.builtin(this.makeRegExp.bind(this)));
this.symbols.registerFunction('@RegExp', ['r'],
ast.builtin(this.ensureType.bind(this, 'RegExp')));

let map = this.symbols.registerSchema('Map', ast.typeType('Any'), undefined, undefined,
['Key', 'Value']);
Expand Down Expand Up @@ -756,16 +756,16 @@ export class Generator {
return ast.snapshotVariable(this.thisIs);
}

// Builtin function - convert string to RegExp
makeRegExp(args: ast.Exp[], params: { [name: string]: ast.Exp }) {
// Builtin function - ensure type of argument
ensureType(type: string, args: ast.Exp[], params: { [name: string]: ast.Exp }) {
if (args.length !== 1) {
throw new Error(errors.application + "RegExp arguments.");
throw new Error(errors.application + "ensureType arguments.");
}
var exp = <ast.ExpValue> this.partialEval(args[0], params);
if (exp.type !== 'String' || !/\/.*\//.test(exp.value)) {
throw new Error(errors.coercion + ast.decodeExpression(exp) + " => RegExp");
if (exp.type !== type) {
throw new Error(errors.coercion + ast.decodeExpression(exp) + " => " + type);
}
return ast.regexp(exp.value);
return exp;
}

// Builtin function - return the parent key of 'this'.
Expand Down
48 changes: 31 additions & 17 deletions src/rules-parser.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -413,24 +413,27 @@ Expression = ConditionalExpression
// ===================================

Literal
= "null" { return ast.nullType() }
/ value:BooleanLiteral { return ast.boolean(value); }
/ value:NumericLiteral { return ast.number(value); }
/ value:StringLiteral { return ast.string(value); }
/ elements:ArrayLiteral { return ast.array(elements); }
= Null
/ BooleanLiteral
/ NumericLiteral
/ StringLiteral
/ ArrayLiteral
/ RegExp

ArrayLiteral = "[" _ elements:ArgumentList? _ "]" { return elements; }
Null = "null" { return ast.nullType() }

ArrayLiteral = "[" _ elements:ArgumentList? _ "]" { return ast.array(elements); }

BooleanLiteral
= "true" { return true; }
/ "false" { return false; }
= "true" { return ast.boolean(true); }
/ "false" { return ast.boolean(false); }

NumericLiteral "number"
= unary:([+-])? literal:(HexIntegerLiteral / DecimalLiteral) {
if (unary == '-') {
return -literal;
return ast.number(-literal);
}
return literal;
return ast.number(literal);
}

DecimalLiteral
Expand Down Expand Up @@ -459,25 +462,36 @@ HexIntegerLiteral = "0" [xX] digits:$HexDigit+ { return parseInt(digits, 16); }

HexDigit = [0-9a-fA-F]

RegExp "regexp" = "/" pattern:RegExpCharacters? "/" modifiers:[a-z]* {
if (modifiers) {
return ast.regexp(pattern, modifiers.join(""));
}
return ast.regexp(pattern);
}

RegExpCharacters = chars:( [^\\/] / RegExpEscaped )+ { return chars.join(""); }

RegExpEscaped = "\\" char_:. { return "\\" + char_; }

StringLiteral "string"
= parts:('"' DoubleStringCharacters? '"' / "'" SingleStringCharacters? "'") {
return parts[1];
}
= parts:('"' DoubleStringCharacters '"' / "'" SingleStringCharacters "'") {
return ast.string(parts[1]);
}

DoubleStringCharacters
= chars:DoubleStringCharacter+ { return chars.join(""); }
= chars:DoubleStringCharacter* { return chars.join(""); }

SingleStringCharacters
= chars:SingleStringCharacter+ { return chars.join(""); }
= chars:SingleStringCharacter* { return chars.join(""); }

DoubleStringCharacter
= !('"' / "\\" / NewLine) char_:. { return char_; }
/ "\\" sequence:EscapeSequence { return sequence; }
/ "\\" sequence:EscapeSequence { return sequence; }
/ LineContinuation

SingleStringCharacter
= !("'" / "\\" / NewLine) char_:. { return char_; }
/ "\\" sequence:EscapeSequence { return sequence; }
/ "\\" sequence:EscapeSequence { return sequence; }
/ LineContinuation

LineContinuation
Expand Down
2 changes: 1 addition & 1 deletion src/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface Window { bolt: any; }
declare var window: Window;
var bolt = (typeof window !== 'undefined' && window.bolt) || require('./bolt');

var MAX_TEST_MS = 30000;
var MAX_TEST_MS = 60000;

export function rulesSuite(suiteName, fnSuite) {
new RulesSuite(suiteName, fnSuite).run();
Expand Down
3 changes: 3 additions & 0 deletions src/test/ast-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ suite("Abstract Syntax Tree (AST)", function() {
[ "'st\\'ring'"],
[ "'st\\ring'" ],
[ "'\\u000d'", "'\\r'" ],
[ "/pattern/" ],
[ "/pattern/i" ],
[ "/pat\\/tern/i" ],
[ "a" ],
[ "a.b" ],
[ "a.b.c" ],
Expand Down
9 changes: 7 additions & 2 deletions src/test/generator-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ suite("Rules Generator Tests", function() {
"multi-update",
"chat",
"serialized",
"map-scalar"
"map-scalar",
"regexp"
];

helper.dataDrivenTest(files, function(filename) {
Expand Down Expand Up @@ -165,7 +166,7 @@ suite("Rules Generator Tests", function() {
expect: "'abc'.toUpperCase()" },
{ data: "this.toUpperCase()",
expect: "newData.val().toUpperCase()" },
{ data: "'ababa'.test('/bab/')",
{ data: "'ababa'.test(/bab/)",
expect: "'ababa'.matches(/bab/)" },
];

Expand Down Expand Up @@ -380,10 +381,14 @@ suite("Rules Generator Tests", function() {
expect: /No type.*NoSuchType/ },
{ data: "path /x { unsupported() { return true; } }",
warn: /unsupported method/i },

{ data: "path /x { validate() { return this.test(123); } }",
expect: /convert value/i },
{ data: "path /x { validate() { return this.test('a/'); } }",
expect: /convert value/i },
{ data: "path /x { validate() { return this.test('/a/'); } }",
expect: /convert value/i },

{ data: "function f(a) { return f(a); } path / { validate() { return f(1); }}",
expect: /recursive/i },
{ data: "type X { $n: Number, $s: String } path / is X;",
Expand Down
8 changes: 8 additions & 0 deletions src/test/parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ suite("Rules Parser Tests", function() {
[ "[1, 2, 3]", ast.array([ast.number(1), ast.number(2), ast.number(3)]) ],
[ "\"string\"", ast.string("string") ],
[ "'string'", ast.string("string") ],
[ "''", ast.string('') ],
[ "/pattern/", ast.regexp("pattern") ],
[ "/pattern/i", ast.regexp("pattern", "i") ],
[ "/pat\\ntern/", ast.regexp("pat\\ntern") ],
[ "/pat\\/tern/", ast.regexp("pat\\/tern") ],
[ "/pat\\tern/", ast.regexp("pat\\tern") ],
];

helper.dataDrivenTest(tests, function(data, expect) {
Expand Down Expand Up @@ -412,6 +418,8 @@ suite("Rules Parser Tests", function() {
expect: /./ },
{ data: "path // is String;",
expect: /./ },
{ data: "path /x { validate() { return this.test(/a/g); } }",
expect: /unsupported regexp modifier/i },
];

helper.dataDrivenTest(tests, function(data, expect) {
Expand Down
Loading

0 comments on commit 84901da

Please sign in to comment.