@kevel/zerkel
v1.9.1
Published
A compiler for the zerkel language.
Keywords
Readme
@kevel/zerkel
The Kevel custom targeting query language: a compiler that turns Zerkel expressions into JavaScript, and a runtime that evaluates the compiled form.
Originally developed in adzerk/zerkel.
Language
Zerkel provides primitive integer and string literals, identifiers, sets, and a selection of boolean operators:
| operator/type | comment | examples |
|-------------------|-----------------------------|------------|
| integers | syntax | 42, -7 |
| strings | syntax | "foo", "C:\\\\Windows\\System32", "^(foo\|bar)\s+" |
| variables | syntax | count, _foo_bar, $location.postalCode |
| . | property accessor | $location.postalCode, foo.bar.baz |
| [, ] | set construction | [42, "foo"] |
| (, ) | expression grouping | x < 100 AND (y < 50 OR z < 10) |
| = | equality | foo = 42 |
| <> | not equal | foo <> "bar" |
| =~ | regex match | foo =~ "^(some\|regular\|expression).*$" |
| !~ | not regex match | foo !~ "^(some\|regular\|expression).*$" |
| > | | foo > 42 |
| < | | foo < 42 |
| >= | | foo >= 42 |
| <= | | foo <= 42 |
| AND | | foo = 42 AND bar < 100 |
| OR | | foo = 42 OR bar < 100 |
| NOT | | NOT (bar =~ "^foo") |
| LIKE | wildcard match | foo LIKE "ba*" |
| CONTAINS | set membership / substring | ["foo", "bar"] CONTAINS "foo", "foobar" CONTAINS "foo" |
Thus, queries may be written like:
count > 43 and (user = "bob" or user = "alice")
keywords contains "awesome"All operators are available in upper and lowercase forms.
Installation
npm install @kevel/zerkelRequires Node.js 24 or later.
Usage
@kevel/zerkel has the same API as the zerkel module of adzerk/zerkel:
const zerkel = require('@kevel/zerkel');
const matches = zerkel.compile('count > 43 and (user = "bob" or user = "alice")');
matches({ count: 50, user: 'bob' }); // true
matches({ count: 12, user: 'alice' }); // false
matches({ count: 50, user: 'george' }); // falseProperties of nested objects can be accessed with .:
const matches = zerkel.compile('user.location = "open field west of a white house"');
matches({ user: { name: 'bob', location: 'open field west of a white house' } }); // true
matches({ user: { name: 'alice', location: 'middle earth' } }); // false| Export | Description |
|---|---|
| compile(expression) | Compiles an expression and returns a Boolean predicate. |
| compileDetailed(expression) | Compiles an expression and returns a detailed predicate. |
| makePredicate(compiled) | Returns a Boolean predicate for an already compiled expression, including a gzipped (GZ:) one. |
| makeDetailedPredicate(compiled) | Returns a detailed predicate for an already compiled expression, including a gzipped one. |
| parser | The compiler: parser.parse(expression) returns the compiled expression as a string. |
compile, compileDetailed and parser.parse throw on an invalid expression.
To store compiled expressions and evaluate them later, compile with
parser.parse and evaluate with makePredicate:
const compiled = zerkel.parser.parse('count > 43');
zerkel.makePredicate(compiled)({ count: 50 }); // trueTwo more entry points hold just the compiler and the runtime:
| Import | Contents |
|---|---|
| @kevel/zerkel/parser | The compiler on its own: parse(expression). |
| @kevel/zerkel/runtime | The runtime on its own: makePredicate(compiled) and makeDetailedPredicate(compiled). It doesn't unzip GZ: strings, and also runs in a browser (see Clientside). |
Detailed evaluation
Use compileDetailed or makeDetailedPredicate when a caller also needs to
know which segment IDs an expression references:
const detailed = zerkel.compileDetailed('$user.segments CONTAINS 42');
detailed({ $user: { segments: [42] } });
// { matched: true, referencedSegmentIds: [42], metadataComplete: true }
detailed({ $user: { segments: [7] } });
// { matched: false, referencedSegmentIds: [42], metadataComplete: true }compileandmakePredicatealways return Boolean predicates;compileDetailedandmakeDetailedPredicatereturn{matched, referencedSegmentIds, metadataComplete}.A reference is collected only from the exact form
$user.segments CONTAINS <integer literal>, across the whole expression (including underNOT,ANDandOR) and independently of evaluation order. Any other use of$user.segments, such as a variable on the right ($user.segments CONTAINS foo), setsmetadataComplete: falsefor the whole expression instead of reporting a partial set.The references are computed at compile time and embedded in the compiled string as a leading
_helpers['captureSegmentMetadata'](...)call, so the compiled form is still a plain string:zerkel.parser.parse('$user.segments CONTAINS 42'); // "(_helpers['captureSegmentMetadata']([42],true),_helpers['idxof']((_env.$user||{}).segments,42))"An expression compiled by an older version has no embedded metadata, so detailed evaluation returns
referencedSegmentIds: []withmetadataComplete: false.
Compatibility
Expressions compiled by older versions run unchanged on this runtime.
Expressions compiled by this version do not run on runtimes older than
1.9.0, because captureSegmentMetadata doesn't exist there. Deploy the runtime
everywhere before newly compiled expressions reach it.
Clientside
Compiled Zerkel expressions can be evaluated in a browser, too. The runtime
(dist/zerkel-runtime.min.js) defines zerkelRuntime when loaded with a
<script> tag, and also works as an AMD or CommonJS module.
In Node.js:
const { parse } = require('@kevel/zerkel/parser');
const compiled = parse('count > 43 and ["bob", "alice"] contains user');In the client:
<script src="zerkel-runtime.min.js"></script>
<script>
var compiled = '...'; // The compiled expression from Node.js above.
var predicateFn = zerkelRuntime.makePredicate(compiled);
var result = predicateFn({count: 50, user: 'bob'});
var detailedFn = zerkelRuntime.makeDetailedPredicate(compiled);
var details = detailedFn({count: 50, user: 'bob'});
</script>To compile in the browser as well, load the browser build after the runtime.
demo/demo.js is the parser plus the module, and sets window.zerkel to
compile, compileDetailed, makePredicate and makeDetailedPredicate, as
adzerk/zerkel's browser build does. As in adzerk/zerkel's index.html, declare
module, exports and require first, because the generated parser writes to
exports:
<script>
var module = {exports: {}};
var exports = module.exports;
var require = function() {};
</script>
<script src="src/zerkel-runtime.js"></script>
<script src="demo/demo.js"></script>
<script>
var matches = zerkel.compile('count > 43');
matches({count: 50}); // true
</script>dist/zerkel-runtime.min.js works in place of src/zerkel-runtime.js. In a
browser there's no zlib, so gzipped (GZ:) expressions can't be evaluated.
Gzip
Set MIN_GZIP_SIZE on the parser to a number, and any compiled expression at
least that long is gzipped, base64-encoded and prefixed with GZ:. The default
is Infinity, so nothing is gzipped unless you opt in.
const zerkel = require('@kevel/zerkel');
zerkel.parser.MIN_GZIP_SIZE = 100;
zerkel.parser.parse('foo = 42');
// "(_helpers['captureSegmentMetadata']([],true),_env.foo==42)"
const compiled = zerkel.parser.parse('[42, 43] contains foo and [100, 200, 300] contains bar');
// "GZ:H4sIAAAAAAAAA..."
zerkel.makePredicate(compiled)({ foo: 42, bar: 100 }); // truemakePredicate and makeDetailedPredicate unzip GZ: strings automatically.
@kevel/zerkel/runtime on its own doesn't, so decompress before using it
directly:
const zlib = require('zlib');
const { makePredicate } = require('@kevel/zerkel/runtime');
function makePredicateFrom(compiled) {
if (compiled.startsWith('GZ:')) {
compiled = zlib.unzipSync(Buffer.from(compiled.slice(3), 'base64')).toString();
}
return makePredicate(compiled);
}The leading captureSegmentMetadata(...) call adds at least 46 characters to
every compiled expression (foo = 42 compiles to 58 characters, up from 12), so a threshold tuned for versions before 1.9.0 will
gzip more expressions than it used to.
Development
From the repository root, install dependencies with npm install (see the
root README). Then, in this directory:
| Command | Description |
|---|---|
| npm run build | Generate dist/zerkel-parser.js (with jison) and dist/zerkel-runtime.min.js (with uglify-js). |
| npm test | Compile the CoffeeScript tests and run them with mocha. The tests load dist/, so run npm run build first. |
| npm run clean | Remove dist/. |
The published package contains dist/ (the module, the generated parser and the minified runtime), the browser build demo/demo.js, the unminified runtime src/zerkel-runtime.js, package.json, this README and LICENSE. demo/demo.js and src/zerkel-runtime.js keep the paths they have in adzerk/zerkel, and @kevel/zerkel/src/zerkel-parser is an alias of @kevel/zerkel/parser, so code that reads or requires those paths works unchanged.
The test suite includes a version-replay check: every corpus in test-data/
(compiled by earlier versions) is evaluated by the current runtime, with both
makePredicate and makeDetailedPredicate. Each run also writes
test-data/<version>.json for the current version.
License
Copyright © Kevel, Inc. Distributed under the Apache License, Version 2.0.
