Commit 983572a6 authored by Artem Sakhatskiy's avatar Artem Sakhatskiy
Browse files

expression added

parents
Pipeline #3636610 failed with stages
in 3 minutes and 3 seconds
/node_modules
/js
/typings
/.DS_Store
image: registry.gitlab.com/thehat/expression.ts:latest
cache:
key: "$CI_BUILD_REF_NAME"
paths:
- node_modules/
stages:
- build
- test
build:
stage: build
script:
- npm install -g tsd typescript
- npm install
- tsd install
- tsc
artifacts:
paths:
- js/
test:
stage: test
script:
- npm test
FROM node:argon
MAINTAINER Artem Sakhatskiy <sakhatskiy@yahoo.com>
RUN npm install -g typescript tslint tsd
RUN npm install -g phantomjs-prebuilt karma-cli
\ No newline at end of file
Expression
========
[![build status](https://gitlab.com/thehat/expression.ts/badges/master/build.svg)](https://gitlab.com/thehat/expression.ts/commits/master)
A wrapper over function containing property expression.
`Expression<TType, TResult>` is an interface that extends `Function`. Module `Expression` contains following methods:
```typescript
/*
* Throws errors if given function is not a valid property expression.
*/
validate<TType, TResult>(exp: Expression<TType, TResult>) : void
/*
* Validates given function and applies it to `target`.
*/
apply<TType, TResult>(exp: Expression<TType, TResult>, target: TType) : TResult
/*
* Validates given function and gets property name. Useful for immutable.js.
*/
getProperty<TType, TResult>(exp: Expression<TType, TResult>) : string
```
Usage
-----
```typescript
class Model
{
value: string;
numberValue: number;
}
let model = new Model();
model.value = "modelThing";
model.numberValue = 12351;
const logInfo = <TResult>(exp: Expression<Model, TResult>) =>
{
Expression.validate(exp);
let property = Expression.getProperty(exp);
console.log("Expression over property: " + property);
console.log("Has a velue ", exp(model));
}
logInfo(m => m.value)
// Expression over property: value
// Has a velue "modelThing"
logInfo(m => m.numberValue)
// Expression over property: numberValue
// Has a velue 12351
logInfo(m => {
console.log("This should throw an error");
})
// Throws: "Expression is not a single return statement. ..."
```
// Karma configuration
// Generated on Sun Jun 12 2016 13:36:18 GMT+0300 (MSK)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'js/**/*.js',
'lib/jasmine.js'
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['mocha'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_WARN,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
phantomjsLauncher: {
// Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom)
exitOnResourceError: true
}
})
}
This diff is collapsed.
{
"name": "expression.ts",
"version": "1.0.0",
"description": "",
"main": "",
"directories": {
"test": "tests"
},
"scripts": {
"test": "karma start --single-run"
},
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/thehat/expression.ts.git"
},
"author": "Artem Sakhatskiy",
"license": "ISC",
"bugs": {
"url": "https://gitlab.com/thehat/expression.ts/issues"
},
"homepage": "https://gitlab.com/thehat/expression.ts#README",
"devDependencies": {
"jasmine": "^2.4.1",
"karma": "^0.13.22",
"karma-jasmine": "^1.0.2",
"karma-mocha-reporter": "^2.0.4",
"karma-phantomjs-launcher": "^1.0.0",
"karma-source-map-support": "^1.1.0",
"phantomjs-prebuilt": "^2.1.7"
}
}
interface Expression<TType, TResult>
{
(target: TType): TResult;
}
module Expression
{
/*
* Validates given function and applies it to `target`.
*/
export const apply = <TType, TResult>(expression: Expression<TType, TResult>, target: TType) =>
{
validate(expression);
return expression(target);
}
/*
* Throws errors if given function is not a valid property expression.
*/
export const validate = <TType, TResult>(expression: Expression<TType, TResult>) =>
{
let sourceCode = expression.toString();
__Internals.validate(sourceCode);
}
/*
* Validates given function and gets property name. Useful for immutable.js.
*/
export const getProperty = <TType, TResult>(expression: Expression<TType, TResult>) =>
{
let sourceCode = expression.toString();
return __Internals.getProperty(sourceCode);
}
export namespace __Internals
{
const ArrowToken = "=>";
const FunctionToken = "function";
const ReturnToken = "return";
interface ExpressionParts
{
token: string;
propertyExpressionBody: string;
}
export const validate = (sourceCode: string) =>
{
if (isLambda(sourceCode))
validateLambda(sourceCode);
else
validateFunction(sourceCode);
}
export const getProperty = (sourceCode: string) =>
{
let expressionParts: ExpressionParts;
if (isLambda(sourceCode))
expressionParts = getLambdaExpressionParts(sourceCode);
else
expressionParts = getFunctionExpressionParts(sourceCode);
return getPropertyFromParts(expressionParts);
}
const isLambda = (sourceCode: string) =>
{
return sourceCode.indexOf(ArrowToken) >= 0;
}
export const validateLambda = (sourceCode: string) =>
{
let expressionParts = getLambdaExpressionParts(sourceCode);
validatePropertyExpressionBody(expressionParts);
}
const getLambdaExpressionParts = (sourceCode: string): ExpressionParts =>
{
let parts = sourceCode
.split(ArrowToken)
.map(part => part.trim());
if (parts.length == 1)
throw new Error(`Source code doesn't contain arrow (${ArrowToken}). Code: "${sourceCode}".`);
if (parts.length >= 3)
throw new Error(`Source code contains too much arrows (${ArrowToken}). Code: "${sourceCode}".`);
let token = parts[0];
let propertyExpressionBody = parts[1];
return {
token: token,
propertyExpressionBody: propertyExpressionBody
};
}
export const validateFunction = (sourceCode: string) =>
{
let expressionParts = getFunctionExpressionParts(sourceCode);
validatePropertyExpressionBody(expressionParts);
}
const getFunctionExpressionParts = (sourceCode: string) : ExpressionParts =>
{
// "function (x){ return x.test; }"
let code = sourceCode.trim();
if (code.indexOf(FunctionToken) != 0)
throw Error(`Expression is not a function. Code: "${sourceCode}".`);
code = code
.substr(FunctionToken.length)
.trim();
// "(x){ return x.test; }"
if (code.indexOf("(") != 0)
throw Error(`Expression is not a function. Code: "${sourceCode}".`);
let indexOfTokenEnd = code.indexOf(")");
if (indexOfTokenEnd < 0)
throw Error(`Expression is not a function. Code: "${sourceCode}".`);
let token = code.substr(1, indexOfTokenEnd - 1);
if (token.indexOf(",") >= 0)
throw Error(`Expression is not a single argument function. Code: "${sourceCode}".`);
let body = code
.substr(indexOfTokenEnd + 1)
.replace("{", "")
.replace("}", "")
.replace(";", "")
.trim();
// "return x.test"
if (body.indexOf(ReturnToken) < 0)
throw Error(`Expression is not a single return statement. Code: "${sourceCode}".`);
body = body
.substr(ReturnToken.length)
.trim();
// "x.test"
return {
token: token,
propertyExpressionBody: body
};
}
const validatePropertyExpressionBody = (expressionParts: ExpressionParts) =>
{
if (!expressionParts.token)
throw new Error("Expression token is empty.");
if (!expressionParts.propertyExpressionBody)
throw new Error("Expression body is empty.");
let property = getPropertyFromParts(expressionParts);
if (!property)
throw new Error("Expression body has no property.");
}
const getPropertyFromParts = (expressionParts: ExpressionParts) =>
{
if (!expressionParts.token)
throw new Error("Expression token is empty.");
if (!expressionParts.propertyExpressionBody)
throw new Error("Expression body is empty.");
let parts = expressionParts.propertyExpressionBody
.split(".")
.map(part => part.trim());
if (parts.length == 1)
throw new Error(`Expression body is not a property expression. Body: "${expressionParts.propertyExpressionBody}".`);
if (parts.length >= 3)
throw new Error(`Expression body is not a simple property expression. Body: "${expressionParts.propertyExpressionBody}".`);
let expressionToken = parts[0];
let property = parts[1];
if (expressionParts.token != expressionToken)
throw new Error(`Expression body is not correlated to token. Body: "${expressionParts.propertyExpressionBody}", token: "${expressionParts.token}".`);
let restrictedSymbols = [",", "(", ")", "{", "}", "+", "-", "!", "?", "<", ">", "*", "/"];
restrictedSymbols.forEach(symbol => {
if (property.indexOf(symbol) >= 0)
throw new Error(`Invalid property expression. Body: "${expressionParts.propertyExpressionBody}".`);
});
return property;
}
}
}
/// <reference path="../typings/tsd.d.ts" />
/// <reference path="../src/expression.ts" />
class A
{
value: string;
func: () => {};
}
describe("An arrow expression source code", () =>
{
it("should cause an error if it isn't a valid arrow function.", () =>
{
let sourceCode = "some => some.simpleString =>";
let isValidArrowFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(isValidArrowFunc).toThrowError(`Source code contains too much arrows (=>). Code: "${sourceCode}".`);
});
it("should cause an error if doesn't contain arrow.", () =>
{
let sourceCode = "some some.simpleString";
let isValidArrowFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(isValidArrowFunc).toThrowError(`Source code doesn't contain arrow (=>). Code: "${sourceCode}".`);
});
it("should cause an error if doesn't contain token.", () =>
{
let sourceCode = " => some.simpleString";
let isValidArrowFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression token is empty.");
});
it("should cause an error if doesn't contain body.", () =>
{
let sourceCode = "some => ";
let isValidArrowFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is empty.");
});
it("should cause an error if body is not correlated to token.", () =>
{
let sourceCode = "some => test.test";
let isValidArrowFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is not correlated to token. Body: \"test.test\", token: \"some\".");
});
it("should cause an error if body is not a property expression", () =>
{
let sourceCode = "some => test";
let isValidArrowFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is not a property expression. Body: \"test\".");
});
it("should cause an error if body is not a simple property expression", () =>
{
let sourceCode = "some => some.test.test2";
let isValidArrowFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is not a simple property expression. Body: \"some.test.test2\".");
});
it("should be a valid arrow function.", () =>
{
let sourceCode = "some => some.simpleString";
let validateFunc = () => Expression.__Internals.validateLambda(sourceCode);
expect(validateFunc).not.toThrow();
});
});
describe("An es3 function expression source code", () =>
{
it("should cause an error if it isn't a valid function.", () =>
{
let sourceCode = "fun (some) { return some.simpleString }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError(`Expression is not a function. Code: "${sourceCode}".`);
});
it("should cause an error if doesn't contain (.", () =>
{
let sourceCode = "function x) { return some.simpleString }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError(`Expression is not a function. Code: "${sourceCode}".`);
});
it("should cause an error if doesn't contain ).", () =>
{
let sourceCode = "function (x { return some.simpleString }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError(`Expression is not a function. Code: "${sourceCode}".`);
});
it("should cause an error if contains many arguments.", () =>
{
let sourceCode = "function (test, test2) { return some.simpleString }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError(`Expression is not a single argument function. Code: "${sourceCode}".`);
});
it("should cause an error if doesn't contain argument.", () =>
{
let sourceCode = "function () { return some.simpleString }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression token is empty.");
});
it("should cause an error if doesn't contain return statement.", () =>
{
let sourceCode = "function (text) { test(); }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError(`Expression is not a single return statement. Code: "${sourceCode}".`);
});
it("should cause an error if doesn't contain body.", () =>
{
let sourceCode = "function (text) { return; }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is empty.");
});
it("should cause an error if body is not correlated to token.", () =>
{
let sourceCode = "function (some) { return test.test; }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is not correlated to token. Body: \"test.test\", token: \"some\".");
});
it("should cause an error if body is not a property expression.", () =>
{
let sourceCode = "function (some) { return test; }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is not a property expression. Body: \"test\".");
});
it("should cause an error if body is not a simple property expression.", () =>
{
let sourceCode = "function (some) { return some.test.test2; }";
let isValidArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(isValidArrowFunc).toThrowError("Expression body is not a simple property expression. Body: \"some.test.test2\".");
});
it("should be a valid function.", () =>
{
let sourceCode = "function (some) { return some.test; }";
let validateArrowFunc = () => Expression.__Internals.validateFunction(sourceCode);
expect(validateArrowFunc).not.toThrow();
});
});
describe("An expression", () =>
{
it("should not be a function call.", () =>
{
let expression: Expression<A, void> = (target) => target.func();
let isValid = () => Expression.validate(expression);