Skip to content

Commit

Permalink
feat: add support for extracting expression from embedded form (#10)
Browse files Browse the repository at this point in the history
Closes #8
  • Loading branch information
char0n committed Oct 31, 2022
1 parent 49a1270 commit b0dae80
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 89 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Runtime Expressions defined in following OpenAPI specification versions:
- [Getting started](#getting-started)
- [Installation](#installation)
- [Usage](#usage)
- [Extraction](#extraction)
- [Parsing](#parsing)
- [Validation](#validation)
- [Grammar](#grammar)
Expand Down Expand Up @@ -45,10 +46,25 @@ you can also install it directly from GitHub.

### Usage

`openapi-runtime-expression` currently supports **parsing** and **validation**.
`openapi-runtime-expression` currently supports **extraction**, **parsing** and **validation**.
Both parser and validator are based on a superset of [ABNF](https://www.rfc-editor.org/rfc/rfc5234) ([SABNF](https://cs.github.com/ldthomas/apg-js2/blob/master/SABNF.md))
and use [apg-js](https://github.com/ldthomas/apg-js) parser generator.

#### Extraction

OpenAPI embeds Runtime Expressions into string values surrounded with `{}` curly braces.
To extract Runtime Expressions from this embedded form, use the **extract** function.
Extracted Runtime Expression can be used for further parsing of validation.

```js
import { extract, test, parse } from 'openapi-runtime-expression';

const expression = extract('{$request.header.accept}'); // => '$request.header.accept'

test(expression); // => true
parse(expression)
```

#### Parsing

Parsing a Runtime Expression is as simple as importing the **parse** function
Expand Down
18 changes: 18 additions & 0 deletions src/extract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* This function is used for extracting the expression from OpenAPI Runtime Expression notation.
*
* @example
*
* extract('{$url}'); // => '$url'
*/

const extract = (openapiRuntimeExpression) => {
if (typeof openapiRuntimeExpression !== 'string') {
return null;
}

const match = openapiRuntimeExpression.match(/^{(?<expression>.+)}$/);
return match?.groups?.expression || null;
};

export default extract;
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Grammar } from './runtime-expression.cjs';
export { default as extract } from './extract.js';
export { default as test } from './test.js';
export { default as parse } from './parse/index.js';
5 changes: 2 additions & 3 deletions src/runtime-expression.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@ tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "
CHAR = unescape /
escape (
%x22 / ; " quotation mark U+0022
%x5C / ; reverse solidus U+005C
%x5C / ; \ reverse solidus U+005C
%x2F / ; / solidus U+002F
%x62 / ; b backspace U+0008
%x66 / ; f form feed U+000C
%x6E / ; n line feed U+000A
%x72 / ; r carriage return U+000D
%x74 / ; t tab U+0009
%x75 4HEXDIG ) ; uXXXX U+XXXX
escape = %x5C ; \
quotation-mark = %x22 ; "
escape = %x5C ; \
unescape = %x20-21 / %x23-5B / %x5D-10FFFF

; Core rules - https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_form
Expand Down
72 changes: 33 additions & 39 deletions src/runtime-expression.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
module.exports = function grammar(){
// ```
// SUMMARY
// rules = 20
// rules = 19
// udts = 0
// opcodes = 104
// opcodes = 103
// --- ABNF original opcodes
// ALT = 11
// CAT = 11
// REP = 6
// RNM = 21
// TLS = 35
// TBS = 11
// TBS = 10
// TRG = 9
// --- SABNF superset opcodes
// UDT = 0
Expand Down Expand Up @@ -47,11 +47,10 @@ module.exports = function grammar(){
this.rules[12] = {name: 'tchar', lower: 'tchar', index: 12, isBkr: false};
this.rules[13] = {name: 'CHAR', lower: 'char', index: 13, isBkr: false};
this.rules[14] = {name: 'escape', lower: 'escape', index: 14, isBkr: false};
this.rules[15] = {name: 'quotation-mark', lower: 'quotation-mark', index: 15, isBkr: false};
this.rules[16] = {name: 'unescape', lower: 'unescape', index: 16, isBkr: false};
this.rules[17] = {name: 'HEXDIG', lower: 'hexdig', index: 17, isBkr: false};
this.rules[18] = {name: 'DIGIT', lower: 'digit', index: 18, isBkr: false};
this.rules[19] = {name: 'ALPHA', lower: 'alpha', index: 19, isBkr: false};
this.rules[15] = {name: 'unescape', lower: 'unescape', index: 15, isBkr: false};
this.rules[16] = {name: 'HEXDIG', lower: 'hexdig', index: 16, isBkr: false};
this.rules[17] = {name: 'DIGIT', lower: 'digit', index: 17, isBkr: false};
this.rules[18] = {name: 'ALPHA', lower: 'alpha', index: 18, isBkr: false};

/* UDTS */
this.udts = [];
Expand Down Expand Up @@ -162,13 +161,13 @@ module.exports = function grammar(){
this.rules[12].opcodes[13] = {type: 7, string: [96]};// TLS
this.rules[12].opcodes[14] = {type: 7, string: [124]};// TLS
this.rules[12].opcodes[15] = {type: 7, string: [126]};// TLS
this.rules[12].opcodes[16] = {type: 4, index: 18};// RNM(DIGIT)
this.rules[12].opcodes[17] = {type: 4, index: 19};// RNM(ALPHA)
this.rules[12].opcodes[16] = {type: 4, index: 17};// RNM(DIGIT)
this.rules[12].opcodes[17] = {type: 4, index: 18};// RNM(ALPHA)

/* CHAR */
this.rules[13].opcodes = [];
this.rules[13].opcodes[0] = {type: 1, children: [1,2]};// ALT
this.rules[13].opcodes[1] = {type: 4, index: 16};// RNM(unescape)
this.rules[13].opcodes[1] = {type: 4, index: 15};// RNM(unescape)
this.rules[13].opcodes[2] = {type: 2, children: [3,4]};// CAT
this.rules[13].opcodes[3] = {type: 4, index: 14};// RNM(escape)
this.rules[13].opcodes[4] = {type: 1, children: [5,6,7,8,9,10,11,12,13]};// ALT
Expand All @@ -183,43 +182,39 @@ module.exports = function grammar(){
this.rules[13].opcodes[13] = {type: 2, children: [14,15]};// CAT
this.rules[13].opcodes[14] = {type: 6, string: [117]};// TBS
this.rules[13].opcodes[15] = {type: 3, min: 4, max: 4};// REP
this.rules[13].opcodes[16] = {type: 4, index: 17};// RNM(HEXDIG)
this.rules[13].opcodes[16] = {type: 4, index: 16};// RNM(HEXDIG)

/* escape */
this.rules[14].opcodes = [];
this.rules[14].opcodes[0] = {type: 6, string: [92]};// TBS

/* quotation-mark */
this.rules[15].opcodes = [];
this.rules[15].opcodes[0] = {type: 6, string: [34]};// TBS

/* unescape */
this.rules[16].opcodes = [];
this.rules[16].opcodes[0] = {type: 1, children: [1,2,3]};// ALT
this.rules[16].opcodes[1] = {type: 5, min: 32, max: 33};// TRG
this.rules[16].opcodes[2] = {type: 5, min: 35, max: 91};// TRG
this.rules[16].opcodes[3] = {type: 5, min: 93, max: 1114111};// TRG
this.rules[15].opcodes = [];
this.rules[15].opcodes[0] = {type: 1, children: [1,2,3]};// ALT
this.rules[15].opcodes[1] = {type: 5, min: 32, max: 33};// TRG
this.rules[15].opcodes[2] = {type: 5, min: 35, max: 91};// TRG
this.rules[15].opcodes[3] = {type: 5, min: 93, max: 1114111};// TRG

/* HEXDIG */
this.rules[17].opcodes = [];
this.rules[17].opcodes[0] = {type: 1, children: [1,2,3,4,5,6,7]};// ALT
this.rules[17].opcodes[1] = {type: 4, index: 18};// RNM(DIGIT)
this.rules[17].opcodes[2] = {type: 7, string: [97]};// TLS
this.rules[17].opcodes[3] = {type: 7, string: [98]};// TLS
this.rules[17].opcodes[4] = {type: 7, string: [99]};// TLS
this.rules[17].opcodes[5] = {type: 7, string: [100]};// TLS
this.rules[17].opcodes[6] = {type: 7, string: [101]};// TLS
this.rules[17].opcodes[7] = {type: 7, string: [102]};// TLS
this.rules[16].opcodes = [];
this.rules[16].opcodes[0] = {type: 1, children: [1,2,3,4,5,6,7]};// ALT
this.rules[16].opcodes[1] = {type: 4, index: 17};// RNM(DIGIT)
this.rules[16].opcodes[2] = {type: 7, string: [97]};// TLS
this.rules[16].opcodes[3] = {type: 7, string: [98]};// TLS
this.rules[16].opcodes[4] = {type: 7, string: [99]};// TLS
this.rules[16].opcodes[5] = {type: 7, string: [100]};// TLS
this.rules[16].opcodes[6] = {type: 7, string: [101]};// TLS
this.rules[16].opcodes[7] = {type: 7, string: [102]};// TLS

/* DIGIT */
this.rules[18].opcodes = [];
this.rules[18].opcodes[0] = {type: 5, min: 48, max: 57};// TRG
this.rules[17].opcodes = [];
this.rules[17].opcodes[0] = {type: 5, min: 48, max: 57};// TRG

/* ALPHA */
this.rules[19].opcodes = [];
this.rules[19].opcodes[0] = {type: 1, children: [1,2]};// ALT
this.rules[19].opcodes[1] = {type: 5, min: 65, max: 90};// TRG
this.rules[19].opcodes[2] = {type: 5, min: 97, max: 122};// TRG
this.rules[18].opcodes = [];
this.rules[18].opcodes[0] = {type: 1, children: [1,2]};// ALT
this.rules[18].opcodes[1] = {type: 5, min: 65, max: 90};// TRG
this.rules[18].opcodes[2] = {type: 5, min: 97, max: 122};// TRG

// The `toString()` function will display the original grammar file(s) that produced these opcodes.
this.toString = function toString(){
Expand All @@ -243,16 +238,15 @@ module.exports = function grammar(){
str += "CHAR = unescape /\n";
str += " escape (\n";
str += " %x22 / ; \" quotation mark U+0022\n";
str += " %x5C / ; reverse solidus U+005C\n";
str += " %x5C / ; \\ reverse solidus U+005C\n";
str += " %x2F / ; / solidus U+002F\n";
str += " %x62 / ; b backspace U+0008\n";
str += " %x66 / ; f form feed U+000C\n";
str += " %x6E / ; n line feed U+000A\n";
str += " %x72 / ; r carriage return U+000D\n";
str += " %x74 / ; t tab U+0009\n";
str += " %x75 4HEXDIG ) ; uXXXX U+XXXX\n";
str += "escape = %x5C ; \\\n";
str += "quotation-mark = %x22 ; \"\n";
str += "escape = %x5C ; \\\n";
str += "unescape = %x20-21 / %x23-5B / %x5D-10FFFF\n";
str += "\n";
str += "; Core rules - https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_form\n";
Expand Down
4 changes: 2 additions & 2 deletions src/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import Grammar from './runtime-expression.cjs';

const grammar = new Grammar();

const test = (str) => {
const test = (str, { strict = false } = {}) => {
if (typeof str !== 'string') {
return false;
}

const apgExp = new ApgExp(grammar);
const apgExp = new ApgExp(grammar, 'y');
return apgExp.test(str);
};

Expand Down
42 changes: 42 additions & 0 deletions test/extract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { assert } from 'chai';

import {extract, test} from '../src/index.js';

describe('extract', function () {
it('should extract expression from OpenAPI runtime expression', function () {
assert.strictEqual(extract('{$url}'), '$url');
assert.strictEqual(extract('{$method}'), '$method');
assert.strictEqual(extract('{$request.path.eventType}'), '$request.path.eventType');
assert.strictEqual(extract('{$request.path.id}'), '$request.path.id');
assert.strictEqual(extract('{$request.query.queryUrl}'), '$request.query.queryUrl');
assert.strictEqual(extract('{$request.header.content-Type}'), '$request.header.content-Type');
assert.strictEqual(extract('{$request.header.accept}'), '$request.header.accept');
assert.strictEqual(extract('{$response.header.Location}'), '$response.header.Location');
assert.strictEqual(extract('{$response.header.Server}'), '$response.header.Server');
assert.strictEqual(extract('{$request.body#/url}'), '$request.body#/url');
assert.strictEqual(extract('{$request.body#/failedUrl}'), '$request.body#/failedUrl');
assert.strictEqual(extract('{$request.body#/successUrls/2}'), '$request.body#/successUrls/2');
assert.strictEqual(extract('{$request.body#/id}'), '$request.body#/id');
assert.strictEqual(extract('{$request.body#/email}'), '$request.body#/email');
assert.strictEqual(extract('{$request.body#/user/uuid}'), '$request.body#/user/uuid');
assert.strictEqual(extract('{$response.body#/uuid}'), '$response.body#/uuid');
assert.strictEqual(extract('{$response.body#/username}'), '$response.body#/username');
assert.strictEqual(extract('{$response.body#/status}'), '$response.body#/status');

assert.strictEqual(extract('{$url $method}'), '$url $method');
assert.strictEqual(extract('{$request.path.eventType}}'), '$request.path.eventType}');
assert.strictEqual(extract('{$request.path.id}}'), '$request.path.id}');
assert.strictEqual(extract('{$request.query.queryUrl}}'), '$request.query.queryUrl}');
assert.strictEqual(extract('{$request.body#/url}}'), '$request.body#/url}');
assert.strictEqual(extract('{$request.body#/user/uuid}}'), '$request.body#/user/uuid}');
});

it('should return null on no match', function () {
assert.isNull(extract('nonsensical string'));
});

it('should return null on invalid input', function () {
assert.isNull(extract(null));
assert.isNull(extract(1));
});
});
Loading

0 comments on commit b0dae80

Please sign in to comment.