Winery: initial version from Bing repo.

This commit is contained in:
Daiyi Peng 2017-12-06 14:14:40 -08:00
Родитель b6a57b5db3
Коммит 26400eaab4
53 изменённых файлов: 22970 добавлений и 31 удалений

21
LICENSE
Просмотреть файл

@ -1,21 +0,0 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

23
LICENSE.txt Normal file
Просмотреть файл

@ -0,0 +1,23 @@
Winery.js
Copyright (c) Microsoft Corporation. All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

564
README.md
Просмотреть файл

@ -1,14 +1,558 @@
# Winery.js
# Contributing
There are already many application frameworks under Node.JS, like express.js, etc. Why do we need another application framework?
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
The short answer is: Winery is to solve different problems.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
As we introduced `napajs`, its major driving scenario was to support intelligent services, which use extensive rules, handcrafted features and machine learned models. This types of services share an experimental nature, which requires fast iteration on changing parameters, replacing algorithms, or even modifying code flows and getting feedback quickly. `wineryjs` is designed as a JSON server that enable applications with the ability of changing behaviors at runtime with maximized flexibility. It doesn't deal with presentation (like HTML, etc.), but solely focused on providing dynamic bindings on parameters, objects and functions during application execution.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
Please note, Winery is a general framework that runs on both Napa and Node.JS. It also provides ability to serve applications across Napa Zones and Node.JS in the same process.
## Installation
```
npm install winery
```
## Quick Start
### **A simple use case**: Running an applcation under current running environment (Node.JS event loop or Napa Zones)
```typescript
import winery = require('winery');
winery.register('example-app', ['example']);
var request: winery.Request = {
application: 'example',
entrypoint: 'echo',
input: 'hello, world'
};
var response: winery.Response = await winery.process(request);
console.log(JSON.stringify(response))
```
### **A more complex use case**: Running an application under Node.JS, dispatching to multiple Napa containers.
```typescript
import napa = require('napajs');
import winery = require('winery');
napa.initialize();
// By using multiple container, we can define different runtime policies.
// Create container1 with default settings.
var zone1 = napa.createZone('zone1', 'main1.js');
// Create container2 with customized settings.
var zone2 = napa.createZone('zone2', 'main2.js', {
cores: 4,
maxStackSize: 1024000,
loggingProvider: '@ms/autopilot'
});
// Serve an io-intensive-app in Node.JS eventloop.
winery.register('io-intensive-app', ['example1'])
// Serve example-app2 using name 'example2' and example-app3 using name 'example3a' in container1.
winery.register('example-app2', ['example2'], container1);
winery.register('example-app3', ['example3a'], container1);
// Serve example-app3 using name 'example3b' and example-app4 using name 'example4' in container2.
winery.register('example-app3', ['example3b'], container2);
winery.register('example-app4', ['example4'], container2);
var request: winery.Request = {
application: 'example1',
entrypoint: 'echo',
input: 'hello, world'
};
var response: winery.Response = await winery.process(request);
console.log(JSON.stringify(response));
```
-----
## Request and Response
`Request`
```json
{
// Required. Application name, which is a value of the names passed to winery.register().
"application": "<application-name>",
// Required. A registered named object of 'Entrypoint' type. see [Entrypoint](#Entrypoint)
"entryPoint": "<entrypoint-name>",
// Optional. Input object for the entrypoint function, the value will be undefined if not specified.
"input": {},
// Optional. Control flags for request serving.
"controlFlags": {
// Optional. Output debugInfo in response. Default set to false.
"debug": true,
// Optional. Output perfInfo in response. Default set to false.
"perf": true
},
// Optional. Override named object consumed by winery.RequestContext.get.
// Through this mechanism, we can change parameters, objects and functions when entrypoint code against named objects.
// Multiple named objects can be overriden in one request.
"overrideObjects": [
{
// Required: name of the object. It should correspond to the name consumed in code.
"name": "<object-name>",
// Required: new value of the object. Can be primitive types, object or input to object creator and providers.
"value": {
}
}
],
// Optional. Override object retrieved by winery.ObjectContext.create.
// see [Object Type](#objectype)
"overrideTypes": [
{
// Required: type name.
"type": "<type-name>",
// Required: module name for the constructor function.
"module": "<module-name>",
// Required: name of the function as constructor.
"constructor": "<function-name-as-constructor>"
}
],
// Optional. Override object (with URI) retrived by winery.ObjectContext.create.
"overrideProviders": [
{
// Required: protocol name. such as 'doc' for URI "doc://<key>".
"protocol": "<protocol-name>",
// Required: module name that defines the function.
"module": "<module-name>",
// Required: function name.
"function": "<function-name>"
}
]
}
```
`Response`
```json
{
// Status code of response.
// Always present.
"responseCode": 0,
// Error message indicate why the request failed.
// Present when responseCode is not 0.
"errorMessage": "<error-message-if-any>",
// Returned object (can be null, primitive type, or object type) from entrypoint.
// Present when returned object is not 'undefined'.
"output": {},
// Present when request.controlFlags.debug is set to true.
"debugInfo": {
// Exception details when responseCode is not 0.
"exception": {
"stack": "<call-stack>",
"message": "<exception-description>",
"fileName": "<source-file-name>",
"lineNumber": 0,
"columnNumber": 0
},
// Logging events when called winery.RequestContext.logger.debug/info/warn/err.
"events": [
{
"time": "<event-time>",
"logLevel": "<log-level>",
"message": "<message>"
}
],
// Probing values when called winery.RequestContext.logger.detail(<key>, <value>).
"details": {
"<debug-key>": "<debug-value>"
}
},
// Present when request.controlFlags.perfInfo is set to true.
"perfInfo": {
"processingLatencyMS": 123
}
}
```
-----
## Writing applications
TODO: use a realworld case as example.
### Step 1: Coding the logics
`example-app/example.ts`
```typescript
import winery = require('winery');
////////////////////////////////////////////////////////////////////////////
/// Functions for entrypoints.
/// Function for entrypoint 'echo'.
/// See 'named-objects.json' below on how we register this entrypoint.
/// The 1st parameter is a winery.RequestContext object.
/// The 2nd parameter is the input from request.
export function echo(context: winery.RequestContext, text: string) {
return text;
}
/// Function for entrypoint 'compute', which is to compute sum on an array of numbers.
export function compute(context: winery.RequestContext, numberArray: number[]) {
var func = (list: number[]) => {
return list.reduce((sum: number, value: number) => {
return sum + value;
}, 0);
}
// Note: context.get will returned named object giving a name.
var functionObject = context.get('customFunction');
if (functionObject != null) {
func = functionObject.value;
}
return func(numberArray);
}
/// Function for entrypoint 'loadObject', which return an object for the uri.
/// NOTE: We use URI to represent object that is able to reference and share more conveniently.
export function loadObject(uri: string, context: winery.RequestContext) {
// Note: context.create will detect uri string and use registered object provider to create the object.
return context.create(uri);
}
/// Function that will be used to provide objects for protocol 'text'.
export function createObject(input: any, context: winery.RequestContext) {
// Note: for non-uri input, context.create will use constructor of registered object types to create it.
return context.create(input);
}
////////////////////////////////////////////////////////////////////////////
/// Functions for object types.
TODO:
////////////////////////////////////////////////////////////////////////////
/// Functions for object providers.
TODO:
```
### Step 2: Configuring Things Together
`example-app/app.json` (root configuration)
```json
{
"id": "example-app",
"description": "Example application for winery",
"objectTypes": ["./object-types.json"],
"objectProviders": ["./object-providers.json"],
"namedObjects": ["./named-objects.json"],
"interceptors": ["./interceptors.json"],
"metrics": {
"sectionName": "ExampleApp",
"definitions": ["./metrics.json"]
}
}
```
`example-app/object-types.json` (a configuration file for objectTypes)
See [[Object Type]](#object-type).
```json
[
{
"typeName": "<type-name>",
"description": "<type-description>",
"moduleName": "<module-name>",
"functionName": "<function-name-as-constructor>",
"schema": "<JSON schema to check object input>"
}
]
```
`example-app/object-providers.json` (a configuration file for objectProviders)
See [[Object Provider]](#object-provider)
```json
[
{
"protocol": "<protocol-name>",
"description": "<protocol-description>",
"moduleName": "<module-name>",
"functionName": "<function-name-as-loader>"
}
]
```
`example-app/named-objects.json` (a configuration file for namedObjects)
See [[Named Object]](#named-object)
```json
[
{
"name": "<object-name>",
// Object value can be created by object factory or providers.
"value": {}
}
]
```
### Step 3 - Trying requests
```json
TODO
```
-----
## Concepts Explained
The core of Winery is to provide mechanisms for overriding behaviors. As behaviors are encapsulated into objects (primitive types, complex types, functions, classes, etc.), overriding behavior would simply be overriding objects.
There are two requirements on overriding objects.
- Override an specific object: We introduced [Named Object](#named-object) to access object by a string key, we can also use the key to override the object.
- Override objects of the same type or provisioned by the same protocol: [Object Type](#object-type) and [Object provider](#object-provider) are designed to satisfy this need.
[Object Context](#object-context) is an interface to expose object creation, provisioning and access-by-name behaviors. Winery implemented a chain of Object Context at different scope (application, request time, etc.) to fulfill an efficient request time overriding.
### Object Context
```typescript
export interface ObjectContext {
/// Create an object from any input.
/// If input is an URI string, it invokes the provider to create the object.
/// If input is an typed object, it invokes the constructor assigned to the type
create(input: any): any;
/// Get an object by name.
get(name: string): any;
}
```
### Object Type
**Interfaces**: ObjectWithType, ObjectConstructor and ObjectFactory
```typescript
/// An object with type has a string property '_type'.
export interface ObjectWithType {
_type: string;
}
/// Object constructor is registered to create object for a specific type.
/// It can access the object context for creating nested objects.
export interface ObjectConstructor {
(input: ObjectWithType | ObjectWithType[], context?: ObjectContext): any;
}
/// Object factory interface, which register many ObjectConstructor with many types.
export interface IObjectFactory {
/// <summary> Construct an output JS value from input object. </summary>
/// <param name="input"> Object with '_type' property or object array. </param>
/// <param name="context"> Context if needed to construct sub-objects. </param>
/// <returns> Any JS value type. </returns>
/// <remarks>
/// When input is array, all items in array must be the same type.
/// On implementation, you can check whether input is array or not as Array.isArray(input).
/// Please refer to example\example_types.ts.
/// </remarks>
construct(input: IObjectWithType | IObjectWithType[], context?: IObjectContext): any;
/// <summary> Check whether current object factory support given type. </summary>
/// <param name="typeName"> value of '_type' property. </param>
/// <returns> True if supported, else false. </returns>
supports(typeName: string): boolean;
}
```
**Registration**: How object type is associated with a object constructor in `object-types.json`
```json
[
{
"type": "<type-name>",
"description": "<description-of-type>",
"moduleName": "<module-name>",
"functionName": "<constructor-function-name>",
"schema": "<JSON-schema-for-input>"
}
]
```
**Usage**: How object type is invoved in object creation.
```typescript
var func = context.create({
"_type": "Function",
"function": "function(a, b){ return a + b; }"
});
// Will print '3'.
console.log(func(1, 2))
```
#### Built-in types.
##### Function
A predefined function object.
```json
{
"_type": "Function",
"moduleName": "a-module",
"functionName": "someNamespace.aFunction"
}
```
or an embeded JavaScript function definition:
```json
{
"_type": "Function",
"function": "function (a, b) { return a + b; }"
}
```
##### Entrypoint
Entrypoint type is a specific function that can be used as entrypoint.
```typescript
export interface EntryPoint {
(input?: any, requestContext?: RequestContext): any
}
```
JSON input to construct an entrypoint.
```json
{
"_type": "Entrypoint",
"moduleName": "a-module",
"functionName": "someNamespace.aEntrypoint",
"displayRank": 1,
"executionStack": [
"finalizeResponse",
"executeEntryPoint"
]
}
```
### Object Provider
Object provider is similar to object constructor, instead of working on object of a type, it creates objects based on protocol from a URI.
For example, in URI "doc://abcde", "doc" is the protocol, "abcde" is path that carry information on what/how the object can be created.
The reason we introduce URI based objets is to advocate a human-readable way to identify and share objects.
**Interfaces**: ObjectLoader and ObjectProvider
```typescript
/// Function to load (create) an object from Uri.
export interface ObjectLoader {
(uri: Uri | Uri[], context?: ObjectContext): any;
}
/// Interface for ObjectProvider.
export interface ObjectProvider {
/// <summary> Provide any JS value from a URI. </summary>
/// <param name="uri"> a URI object or array of URIs. </param>
/// <param name="context"> Object context if needed to create sub-objects. </param>
/// <returns> Any JS value. </returns>
/// <remarks>
/// On implementation, you can check whether input is array or not as Array.isArray(input).
/// Please refer to example\example_providers.ts.
/// </remarks>
provide(uri: Uri | Uri[], context?: ObjectContext): any;
/// <summary> Check if current provider support a protocol name.</summary>
/// <param name="protocol"> Case insensitive protocol name. </param>
/// <returns> True if protocol is supported, otherwise false. </param>
supports(protocol: string): boolean;
}
```
**Registration**: protocol and object provider are associated together via `object-providers.json` from 'objectProviders' element of root configuration.
```json
[
{
"protocol": "<protocol-name>",
"description": "<protocol-description>",
"moduleName": "<module-name>",
"functionName": "<function-name-as-loader>"
}
]
```
**Usage**: How object provider is invovled in object creation.
```typescript
var doc = context.create('doc://example-doc');
// Will print { "id": "example-doc-01", title: "xxx", ...}
console.log(JSON.stringify(doc));
```
### Named Object
Named object is introduced to access well-known objects in system by a string name.
**Interfaces**: NamedObjectDefinition, NamedObject and NamedObjectCollection
```typescript
export class NamedObjectDefinition {
public name: string = null;
public description: string = null;
public isPrivate: boolean = false;
public override: boolean = false;
public value: any = null;
}
export class NamedObject {
/// <summary> Constructor </summary>
/// <param name="definition"> Definition of current named object. </param>
/// <param name="value"> Value of current named object </param>
public constructor(public definition: NamedObjectDefinition, public value: any) {
this.definition = definition;
this.value = value;
}
}
export interface NamedObjectCollection {
/// <summary> Get named object by name. </summary>
/// <param name="name"> Name. Case-sensitive. </summary>
/// <returns> Named object if found. Otherwise undefined. </returns>
get(name: string): NamedObject;
}
```
**Registration**: Registering named object in `named-objects.json`.
Named object can be created automatically from JSON (with the support of ObjectType and ObjectProvider). In configuration, you can register named objects in `named-objects.json`, which is included from 'namedObjects' element of root configuration.
Please note: Entrypoint registration is simply a named object registration.
```json
[
{
"name": "echo",
// Entrypoint, created as an object with type (Entrypoint).
"value": {
"_type": "EntryPoint",
"moduleName": "./example",
"functionName": "echo"
}
},
{
"name": "example-doc",
// Object created by object provider for protocol "doc".
"value": "doc://example-document"
}
]
```
**Usage**: How named object is involved in object access.
```typescript
var object = context.get('example-doc');
console.log(JSON.)
```
# Contribute
You can contribute to Winery.js in following ways:
* [Report issues](https://github.com/Microsoft/wineryjs/issues) and help us verify fixes as they are checked in.
* Review the [source code changes](https://github.com/Microsoft/wineryjs/pulls).
* Contribute bug fixes.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact opencode@microsoft.com with any additional questions or comments.
# License
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [MIT](https://github.com/Microsoft/napajs/blob/master/LICENSE.txt) License.

Просмотреть файл

@ -0,0 +1,139 @@
[
{
"name": "listApplications",
"description": "List all applications in current system.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.listApplications",
"displayRank": 900,
"exampleRequests": [
{
"application": "example",
"entryPoint": "listApplications"
}
]
}
},
{
"name": "listEntryPoints",
"description": "List all entry points for current application.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.listEntryPoints",
"displayRank": 900,
"exampleRequests": [
{
"application": "bing",
"entryPoint": "listEntryPoints"
}
]
}
},
{
"name": "listNamedObjects",
"description": "List all named objects for current application.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.listNamedObjects",
"displayRank": 900,
"exampleRequests": [
{
"application": "bing",
"entryPoint": "listNamedObjects"
}
]
}
},
{
"name": "listTypes",
"description": "List all types supported in current application.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.listTypes",
"displayRank": 900,
"exampleRequests": [
{
"application": "bing",
"entryPoint": "listTypes"
}
]
}
},
{
"name": "listProviders",
"description": "List URI providers supported in current application.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.listProviders",
"displayRank": 900,
"exampleRequests": [
{
"application": "bing",
"entryPoint": "listProviders"
}
]
}
},
{
"name": "getNamedObject",
"description": "Get an object as JSON based on current application.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.getNamedObject",
"displayRank": 900,
"exampleRequests": [
{
"application": "bing",
"entryPoint": "getNamedObject",
"input": {
"name": "getType"
}
}
]
}
},
{
"name": "getType",
"description": "Get definition of a type in current application.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.getType",
"displayRank": 900,
"exampleRequests": [
{
"application": "bing",
"entryPoint": "getType",
"input": {
"typeName": "functionName"
}
}
]
}
},
{
"name": "getProvider",
"description": "Get the provider definition for a URI protocol.",
"value": {
"_type": "EntryPoint",
"moduleName": "winery",
"functionName": "builtins.entryPoints.getProvider",
"displayRank": 900,
"exampleRequests": [
{
"application": "bing",
"entryPoint": "getProvider",
"input": {
"protocolName": "te"
}
}
]
}
}
]

Просмотреть файл

@ -0,0 +1,65 @@
[
{
"name": "passThrough",
"description": "Interceptor to pass through current interception. ",
"value": {
"_type": "Interceptor",
"moduleName": "winery",
"functionName": "builtins.interceptors.passThrough"
}
},
{
"name": "shortCircuit",
"description": "Interceptor to short circuit execution. ",
"value": {
"_type": "Interceptor",
"moduleName": "winery",
"functionName": "builtins.interceptors.shortCircuit"
}
},
{
"name": "executeEntryPoint",
"description": "Interceptor to execute entrypoint from request context. ",
"value": {
"_type": "Interceptor",
"moduleName": "winery",
"functionName": "builtins.interceptors.executeEntryPoint"
}
},
{
"name": "finalizeResponse",
"description": "Interceptor to finalize response. ",
"value": {
"_type": "Interceptor",
"moduleName": "winery",
"functionName": "builtins.interceptors.finalizeResponse"
}
},
{
"name": "logRequest",
"description": "Interceptor to log request. ",
"value": {
"_type": "Interceptor",
"moduleName": "winery",
"functionName": "builtins.interceptors.logRequest"
}
},
{
"name": "logResponse",
"description": "Interceptor to log response. ",
"value": {
"_type": "Interceptor",
"moduleName": "winery",
"functionName": "builtins.interceptors.logResponse"
}
},
{
"name": "logRequestResponse",
"description": "Interceptor to log request and response. ",
"value": {
"_type": "Interceptor",
"moduleName": "winery",
"functionName": "builtins.interceptors.logRequestResponse"
}
}
]

67
config/builtin-types.json Normal file
Просмотреть файл

@ -0,0 +1,67 @@
[
{
"typeName": "Function",
"description": "Constructor for Function object.",
"moduleName": "winery",
"functionName": "builtins.types.createFunction",
"schema": "winery/schema/function-def.schema.json",
"exampleObjects": [
{
"_type": "Function",
"function": "function() { return 0; }"
},
{
"_type": "Function",
"moduleName": "someModule",
"functionName": "someNamespace.someFunction"
}
]
},
{
"typeName": "EntryPoint",
"description": "Entrypoint type .",
"moduleName": "winery",
"functionName": "builtins.types.createEntryPoint",
"schema": "winery/schema/entrypoint-def.schema.json",
"exampleObjects": [
{
"_type": "EntryPoint",
"moduleName": "someModule",
"functionName": "someNamespace.someFunction",
"displayRank": 100
// Using default execution stack.
},
{
"_type": "EntryPoint",
"moduleName": "someModule",
"functionName": "someNamespace.someFunction",
"displayRank": 100,
// Using custom execution stack.
"executionStack": [
"logRequestResponse",
"finalizeResponse",
"executeEntryPoint"
]
}
]
},
{
"typeName": "Interceptor",
"description": "Interceptor is a function defined as: (requestContext: RequestContext) => Promise<Response>.",
"moduleName": "winery",
"functionName": "builtins.types.createInterceptor",
"schema": "winery/schema/interceptor-def.schema.json",
"exampleObjects": [
{
"_type": "Interceptor",
"moduleName": "someModule",
"functionName": "someNamespace.someFunction"
},
{
"_type": "Interceptor",
"function": "function(context) { return context.continueExecution(); } "
}
]
}
]

32
config/engine.json Normal file
Просмотреть файл

@ -0,0 +1,32 @@
///////////////////////////////////////////////////////////////
// This file contains root settings for winery.
// Users can do per-application override of these values.
{
// Allow per-request override or not.
"allowPerRequestOverride": true,
// Throw exception on error or return response with error code.
"throwExceptionOnError": true,
// Default execution stack of interceptors if not specified per-application or per-entryPoint.
"defaultExecutionStack": [
"finalizeResponse",
"executeEntryPoint"
],
// Default available object types.
"objectTypes": [
"./builtin-types.json"
],
// Default available object providers.
"objectProviders": [
],
// Default available named objects.
"namedObjects": [
"./builtin-interceptors.json",
"./builtin-entrypoints.json"
]
}

5179
lib/ajv-bundle.js Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

694
lib/app.ts Normal file
Просмотреть файл

@ -0,0 +1,694 @@
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This file contains application serving code for winery.
//
// Each application sub-directory starts with an 'app.json', which is the root JSON file of this application.
// The 'app.json' declares several aspects of an application, described by './schema/application-config.schema.json'.
//
// Commonly defined aspects of an application are:
// 1) Object model - Facilitating object creation, provisioning, overriding and retrieval within application functions.
// a. 'objectTypes': Define constructor of object types supported in current application.
// b. 'objectProviders': Define URI based object providers in current applicaton.
// On provide() implementation, you can use two inputs, one is IObjectWithType and the other
// is IObjectWithType array. When you use an array as input, all items must be the same type.
// c. 'namedObjects': Define objects that can be retrieved by global name in current application.
// Like entry points of application, parameters etc.
// On construct() implementation, you have two input options, Uri or Uri array.
//
// In 'app.json' Each of these elements can include multiple separate JSON files, which enables sharing on
// common definitions across multiple applications. Overriding between multiple JSON files are also supported,
// an 'override' property needs to be set to true if we want to override an definition from entries
// in latter file against former ones.
//
// Common types and named objects are provided. Common types are "Function", "EntryPoint". "NamedObjectRef".
// And common named objects are shared commands such as "listAllEntryPoints", "listAllNamedObjects", etc.
//
// All the concepts in object model could be overridden at request time, which enables us to alter system behavior
// at request level. We can change system parameter by overriding a numeric named object, or experiment some ad-hoc
// code by overriding a function named object in system. We can also override object constructor and providers to detour
// data creation from some hard dependencies to simple local implementations for testing purpose.
//
// 2) Application level resources,such as:
// a) Metrics
// b) Logging
// c) More.
//
// 3) Application policies, such as
// a) Throttle control policy.
// b) More.
//
// Beyond the JSON definition that makes predefined application capability declarative, if there is any application specific
// properties for an application, application developers can always add properties to their applcation object in "<app-name>\app.ts".
//
// Application object is exposed as a global object, which resides in each V8 isolate and is alive since the application is initialized.
// Developers can retrieve a specific application by name via Application.getApplication(name)
// or current application from RequestContext.getApplication().
// External dependencies.
// TODO: implement os.hostname(), it's used for debugging.
import * as os from 'os';
import * as path from 'path';
import * as assert from 'assert';
import * as napa from 'napajs';
import * as logger from '@napajs/logger';
import * as metrics from '@napajs/metrics';
// internal dependencies.
import * as objectContext from './object-context';
import * as wire from './wire';
import * as utils from './utils';
import * as config from './config'
import { NamedObject } from './named-object';
/////////////////////////////////////////////////////////////////////////////////////////
/// Application Engine for managing all applications.
/// <summary> Interface for global winery settings. </summary>
export interface Settings {
/// <summary> Allow per-request override. Default is true. </summary>
allowPerRequestOverride: boolean;
/// <summary> Global scoped context definition. </summary>
objectContext: objectContext.ScopedObjectContextDefinition;
/// <summary> Default execution stack for all applications. Applications, entrypoints can override. </summary>
defaultExecutionStack?: string[];
/// <summary> Base directory to resolve relative paths. </summary>
baseDir: string;
}
///////////////////////////////////////////////////////////////////////////////
/// Interfaces and classes for Application.
/// <summary> Interface for entrypoint function
/// Entrypoint is a function that perform application logic.
/// Its return value will be property 'output' of Response.
/// Entrypoint can be synchronous function or asynchronous functions.
/// For asynchronous function, always return a Promise.
/// </summary>
export type EntryPoint = (requestContext?: RequestContext, input?: any) => any;
/// <summary> Interface for interceptor
/// Interceptor is the execution unit of winery, multiple interceptors are stacked in execution,
/// That outer interceptors can short circuit the execution. This pattern is useful to add policy layers.
/// during execution, like throttle control, access control, instrumentation etc.
/// Following diagram depicts how multiple interceptors are organized together into an execution stack.
///
/// Execution stack:
/// +------------------------------------------------------------------------------+
/// | Interceptor 1 Do pre-request work 1 |
/// | +-----------------------------------------------------------+ |
/// | | Interceptor 2 Do pre-request work 2 | |
/// | | +----------------------------------------------+ | |
/// | | | (More interceptors) ... | | |
/// | | | +----------------------------+ | | |
/// | | | | Interceptor N | | | |
/// | | | | (Entrypoint execution) | | | |
/// | | | +----------------------------+ | | |
/// | | +----------------------------------------------+ | |
/// | | Do post-response work 2 | |
/// | +-----------------------------------------------------------+ |
/// | Do post-response work 1 |
/// +------------------------------------------------------------------------------+
///
/// Interceptor can be sync (always return a resolved promise) or async (with unresolved promise).
/// Always implement an interceptor in following patterns:
/// 1) Do pre-request work (can be optional).
/// 2) call 'let response = await context.continueExecution()' or short circuit.
/// 3) Do post-request work (can be optional).
/// </summary>
export type Interceptor = (context: RequestContext) => Promise<wire.Response>;
/// <summary> Interface for metric collection. </summary>
export type MetricCollection = { [name: string]: metrics.Metric };
/// <summary> Class for Metric definition. </summary>
export interface MetricDefinition {
/// <summary> Name used to access this metric via context.metric['<name>'] </summary>
name: string;
/// <summary> Section e for the metric, which is passed to create the metric. </summary>
sectionName: string;
/// <summary> Display name for the metric, which is passed to create the metric. </summary>
displayName: string;
/// <summary> Description for this metric. For human consumption purpose. </summary>
description?: string;
/// <summary> Metric type. </summary>
type: metrics.MetricType;
/// <summary> Dimension definitions. </summary>
dimensionNames?: string[];
}
/// <summary> Class for settings of an application </summary>
export interface ApplicationSettings extends Settings {
/// <summary> ID of application.
/// To distinguish from name, which is associated with application instance at runtime by AppEngine.register(),
/// ID is used for identifying the purpose of application, usually we can put module name as ID.
/// </summary>
id: string;
/// <summary> Description of application. </summary>
description?: string;
/// <summary> Definition of metrics in this application. </summary>
metrics: MetricDefinition[];
}
/// <summary> Class for Napa application. </summary>
export class Application {
/// <summary> Application settings. </summary>
private _settings: ApplicationSettings;
/// <summary> Per-application object context. </summary>
private _perAppObjectContext: objectContext.ScopedObjectContext;
/// <summary> Default execution stack if not specified per-entrypoint. </summary>
private _defaultExecutionStack: Interceptor[];
/// <summary> Metric collection. </summary>
private _metrics: MetricCollection;
/// <summary> Per-entrypoint execution stack when there is no per-request override. </summary>
private _perEntryPointExecutionStack: Map<string, Interceptor[]>;
/// <summary> Construct application from application settings. </summary>
/// <param name="engine"> Application engine that run current application. </summary>
/// <param name="settings"> Application settings. </summary>
public constructor(
parentContext: objectContext.ScopedObjectContext,
settings: ApplicationSettings) {
this._settings = settings;
this._perAppObjectContext = new objectContext.ScopedObjectContext(
"application",
settings.baseDir,
parentContext,
settings.objectContext);
// Create default execution stack.
this._defaultExecutionStack = [];
// Prepare default execution stack.
for (let interceptorName of this._settings.defaultExecutionStack) {
let interceptor = this.getInterceptor(interceptorName);
if (interceptor == null) {
throw new Error("Interceptor does not exisit: '" + interceptorName + "'.");
}
this._defaultExecutionStack.push(interceptor);
}
// Prepare per-entrypoint execution stack.
this._perEntryPointExecutionStack = new Map<string, Interceptor[]>();
this._perAppObjectContext.forEach(object => {
if (object.definition.value._type === 'EntryPoint') {
let executionStack: Interceptor[] = this._defaultExecutionStack;
let customStack = object.definition.value.executionStack;
// Entrypoint has specified executionStack.
if (customStack != null) {
executionStack = [];
for (let interceptorName of <string[]>(customStack)) {
let interceptor = this.getInterceptor(interceptorName);
if (interceptor == null) {
throw new Error("Interceptor does not exist: '" + interceptorName + "'");
}
executionStack.push(interceptor);
}
}
this._perEntryPointExecutionStack.set(
object.definition.name,
executionStack);
}
});
// Create metrics.
this._metrics = {};
if (settings.metrics != null) {
for (let metric of settings.metrics) {
this._metrics[metric.name] = metrics.get(
metric.sectionName,
metric.displayName,
metric.type,
metric.dimensionNames);
}
}
}
/// <summary> Get application ID. </summary>
/// <returns> Application ID. </returns>
public get id(): string {
return this._settings.id;
}
/// <summary> Get application description. </summary>
/// <returns> Application description. </returns>
public get description(): string {
return this._settings.description;
}
/// <summary> Get Application settings. </summary>
/// <returns> Application settings. </returns>
public get settings(): ApplicationSettings {
return this._settings;
}
/// <summary> Get application level object context. </summary>
/// <returns> Application level object context. </returns>
public get objectContext(): objectContext.ScopedObjectContext {
return this._perAppObjectContext;
}
/// <summary> Get default execution stack. </summary>
/// <returns> Interceptor list configured as default stack. </returns>
public get defaultExecutionStack(): Interceptor[] {
return this._defaultExecutionStack;
}
/// <summary> Get execution stack for an entrypoint before any request override. </summary>
/// <param name="entryPointName"> Entrypoint name. </param>
/// <returns> Execution stack. </returns>
public getExecutionStack(entryPointName: string): Interceptor[] {
if (this._perEntryPointExecutionStack.has(entryPointName)) {
return this._perEntryPointExecutionStack.get(entryPointName);
}
return null;
}
/// <summary> Get metric collection of this application. </summary>
/// <returns> Metric collection of current application. </summary>
public get metrics(): MetricCollection {
return this._metrics;
}
/// <summary> Create object from input. Throw exception if creation failed. </summary>
/// <param name="input"> Any JS value </param>
/// <returns> JS value created. </returns>
public create(input: any): any {
return this._perAppObjectContext.create(input);
}
/// <summary> Get the value of a named object. </summary>
/// <param name='name'> Name of the object. Case sensitive. </param>
/// <returns> Value of the named object or null if not found. </returns>
public get(name: string): any {
let namedObject = this.getNamedObject(name);
if (namedObject != null) {
return namedObject.value;
}
return null;
}
/// <summary> Get application level named object. </summary>
/// <param name="name"> Name. Case-sensitive. </param>
/// <returns> Named object if found. Otherwise undefined. </returns>
public getNamedObject(name: string): NamedObject {
return this._perAppObjectContext.get(name);
}
/// <summary> Get entry point from current application. Throws exception if entry point is not found. </summary>
/// <param name="entryPointName"> Entry point name, case sensitive. </param>
/// <returns> Entrypoint (function) if found. Otherwise throws exception. </returns>
public getEntryPoint(entryPointName: string): EntryPoint {
let object = this.getNamedObject(entryPointName);
if (object != null && object.definition.value._type != 'EntryPoint') {
throw new Error("Object '" + entryPointName + "' is not of EntryPoint type. Encountered: '" + object.definition.name + "'.");
}
return object != null? object.value : null;
}
/// <summary> Get interceptor by name. </summary>
/// <param name="name"> Interceptor name. </param>
/// <returns> Interceptor object if found, undefined otherwise. </returns>
public getInterceptor(name: string): Interceptor {
let object = this.getNamedObject(name);
if (object != null && object.definition.value._type !== 'Interceptor') {
throw new Error("Object '" + name + "' is not of Interceptor type. Encountered: '" + object.definition.name + "'.");
}
return object == null? null: object.value;
}
/// <summary> Get a function as a named object from current application. Throws exception if function is not found or not a function object. </summary>
/// <param name="functionName"> Function name, case sensitive. </param>
/// <returns> Function object if found. Otherwise throws exception. </returns>
public getFunction(functionName: string): any {
let object = this.getNamedObject(functionName);
let fun = object == null ? null : object.value;
if (fun != null && typeof fun !== 'function') {
throw new Error("Object '" + functionName + "' is not a function.");
}
return fun;
}
}
///////////////////////////////////////////////////////////////////////
/// Classes for request serving.
/// <summary> Class for request context.
/// Request context is the access point for all winery capabilities during serving a request.
/// These capabilities are:
/// 1) Object creation and accesses: via create() and get()/getNamedObject().
/// </summary>
export class RequestContext {
/// <summary> Application</summary>
private _application: Application = null;
/// <summary> Request</summary>
private _request: wire.Request = null;
/// <summary> Entry point </summary>
private _entryPoint: EntryPoint = null;
/// <summary> Request level object context. </summary>
private _perRequestObjectContext: objectContext.ScopedObjectContext = null;
/// <summary> Per request logger. </summary>
private _logger: RequestLogger = null;
/// <summary> Debugger. </summary>
private _debugger: Debugger = null;
/// <summary> Execution state: current depth in execution stack. </summary>
private _executionDepth: number = 0;
/// <summary> Execution stack.
private _executionStack: Interceptor[];
/// <summary> Constructor </summary>
public constructor(app: Application, request: wire.Request) {
// Fill default values and do schema validation.
request = wire.RequestHelper.fromJsValue(request);
this._application = app;
this._request = request;
// We only pass overriden stuff when per-request override is allowed.
let perRequestObjectContextDef: objectContext.ScopedObjectContextDefinition =
app.settings.allowPerRequestOverride ?
new objectContext.ScopedObjectContextDefinition(
app.settings.objectContext,
request.overrideTypes,
request.overrideProviders,
request.overrideObjects,
false // Don't do dependency analysis at request level.
)
:
new objectContext.ScopedObjectContextDefinition(
app.settings.objectContext,
[],
[],
[],
false);
this._perRequestObjectContext = new objectContext.ScopedObjectContext(
"request",
app.settings.baseDir,
app.objectContext,
perRequestObjectContextDef);
// Prepare execution stack and entry point.
this._entryPoint = this.getEntryPoint(request.entryPoint);
if (this._entryPoint == null) {
throw new Error("Entrypoint does not exist: '" + request.entryPoint + "'");
}
this._executionStack = this.prepareExecutionStack(request.entryPoint);
// Prepare logger and debuger.
this._logger = new RequestLogger(
request.application + "." + request.entryPoint,
request.traceId);
this._debugger = new Debugger();
// Set execution depth to 0 to be at top of execution stack.
this._executionDepth = 0;
}
/// <summary> prepare execution stack for an entrypoint name, assuming per-request object context is setup. </summary>
private prepareExecutionStack(entryPointName: string): Interceptor[] {
// If nothing has been overrided, use cached execution stack directly.
if (this._perRequestObjectContext.definition.namedObjects.length == 0) {
if (this._entryPoint == null) {
throw new Error("Entrypoint '" + entryPointName + "' does not exist.");
}
return this._application.getExecutionStack(entryPointName);
}
// Per-request override happens, it could be entrypoint override or interceptor override.
let entryPointObject = this.getNamedObject(entryPointName);
let interceptorNames: string[] = entryPointObject.definition.value.executionStack;
if (interceptorNames == null) {
interceptorNames = this.application.settings.defaultExecutionStack;
}
// When entrypoint is not overriden, check if interceptor definition has be overriden.
if (entryPointObject.scope !== 'request') {
let oldStack = this.application.getExecutionStack(entryPointName);
let newStack: Interceptor[] = [];
// Definition and pre-cached execution stack should align.
assert(oldStack.length == interceptorNames.length);
for (let i = 0; i < oldStack.length; ++i) {
let interceptorName = interceptorNames[i];
let interceptorObject = this.getNamedObject(interceptorName);
if (interceptorObject == null) {
throw("Interceptor '" + interceptorName + "' does not exist.");
}
if (interceptorObject.scope !== 'request') {
newStack.push(oldStack[i]);
}
else {
// Interceptor is overriden from request. Look up new.
if (interceptorObject.value == null
|| interceptorObject.definition.value._type !== 'Interceptor') {
throw new Error("Bad override on interceptor '"
+ interceptorName
+ "', should be of Interceptor type and not null. ")
}
newStack.push(interceptorObject.value);
}
}
return newStack;
}
// Per-request override happens on current entry point.
let newStack: Interceptor[] = [];
for (let interceptorName of interceptorNames) {
let interceptor = this.getInterceptor(interceptorName);
if (interceptor == null) {
throw new Error("Interceptor '" + interceptorName + "' is not a valid interceptor.");
}
newStack.push(interceptor);
}
return newStack;
}
///////////////////////////////////////////////////////////////////
/// Operational interfaces
/// <summary> Execute current request with a promise of response. </summary>
public async execute(): Promise<wire.Response> {
return this.continueExecution();
}
/// <summary> Continue execution from current interceptor. </summary>
public async continueExecution(): Promise<wire.Response> {
if (this._executionDepth < this._executionStack.length) {
return this._executionStack[this._executionDepth++](this);
}
return Promise.resolve({ responseCode: wire.ResponseCode.Success });
}
///////////////////////////////////////////////////////////////////
/// Informational interfaces
/// <summary> Get application of current request. </summary>
public get application(): Application {
return this._application;
}
/// <summary> Get the request used to create this context. </summary>
public get request(): wire.Request {
return this._request;
}
/// <summary> Entrypoint </summary>
public get entryPoint(): EntryPoint {
return this._entryPoint;
}
/// <summary> Get entry point name. </summary>
public get entryPointName(): string {
return this._request.entryPoint;
}
/// <summary> Get trace ID of current request. </summary>
public get traceId(): string {
return this._request.traceId;
}
/// <summary> Get control flags. </summary>
public get controlFlags(): wire.ControlFlags {
return this._request.controlFlags;
}
/// <summary> getter for metric collection. </summary>
public get metric(): MetricCollection {
return this._application.metrics;
}
/// <summary> Get input for entry point. </summary>
public get input(): any {
return this._request.input;
}
/// <summary> Get per request logger. </summary>
public get logger(): RequestLogger {
return this._logger;
}
/// <summary> Get debug info writter. </summary>
public get debugger(): Debugger {
return this._debugger;
}
///////////////////////////////////////////////////////////////
/// Behavioral interfaces
/// <summary> Create object from input. </summary>
/// <param name='input'> Input for creating object with type or URI </param>
/// <returns> Created object. </returns>
public create(input: any): any {
return this._perRequestObjectContext.create(input);
}
/// <summary> Get the value of a named object. </summary>
/// <param name='name'> Name of the object. Case sensitive. </param>
/// <returns> Value of the named object or null if not found. </returns>
public get(name: string): any {
let namedObject = this.getNamedObject(name);
if (namedObject != null) {
return namedObject.value;
}
return null;
}
/// <summary> Get named object from input. </summary>
/// <param name='name'> Name of the object. Case sensitive. </param>
/// <returns> Named object or null if not found. </returns>
public getNamedObject(name: string): NamedObject {
return this._perRequestObjectContext.get(name);
}
/// <summary> Helper method to get entry point from application of request context. Throws exception if entry point is not found. </summary>
/// <param name="entryPointName"> Entry point name, case sensitive. </param>
/// <returns> Entrypoint (function) if found. Otherwise throws exception. </returns>
public getEntryPoint(entryPointName: string): EntryPoint {
let object = this.getNamedObject(entryPointName);
if (object != null && object.definition.value._type != 'EntryPoint') {
throw new Error("Object '" + entryPointName + "' is not of EntryPoint type.");
}
return object != null? object.value : null;
}
/// <summary> Get interceptor by name. </summary>
/// <param name="name"> Interceptor name. </param>
/// <returns> Interceptor object if found, undefined otherwise. </returns>
public getInterceptor(name: string): Interceptor {
let object = this.getNamedObject(name);
if (object != null && object.definition.name !== 'Interceptor') {
throw new Error("Object '" + name + "' is not of Interceptor type.");
}
return object == null? null: object.value;
}
/// <summary> Helper method to get function from application of request context. Throws exception if function is not found or not a function object. </summary>
/// <param name="functionName"> Function name, case sensitive. </param>
/// <returns> Function object or null. If object associated with functionName is not a function, exception will be thrown. </returns>
public getFunction(functionName: string): any {
let func = this.get(functionName);
if (func != null && typeof func !== 'function') {
throw new Error("Object '" + functionName + "' is not a function.");
}
return func;
}
}
/// <summary> Class for debugger, which writes debugInfo in response. </summary>
export class Debugger {
/// <summary> Set last error that will be output in debug info. </summary>
public setLastError(lastError: Error) {
this._lastError = lastError;
}
/// <summary> Output an object with a key in debugInfo/details. </summary>
public detail(key: string, value: any): void {
this._details[key] = value;
}
/// <summary> Add a debug event with a log level and message. </summary>
public event(logLevel: string, message: string): void {
this._events.push({
eventTime: new Date(),
logLevel: logLevel,
message: message
});
}
/// <summary> Finalize debug info writer and return a debug info. </summary>
public getOutput(): wire.DebugInfo {
return {
exception: {
message: this._lastError.message,
stack: this._lastError.stack,
},
details: this._details,
events: this._events,
machineName: os.hostname(),
};
}
private _lastError: Error = null;
private _details: {[key: string]: any} = {};
private _events: wire.DebugEvent[] = [];
}
/// <summary> Request logger that encapsulate section name and trace ID. </summary>
export class RequestLogger {
public constructor(sectionName: string, traceId: string) {
this._sectionName = sectionName;
this._traceId = traceId;
}
/// <summary> Log message with Debug level. </summary>
public debug(message: string) {
logger.debug(this._sectionName, this._traceId, message);
}
/// <summary> Log message with Info level. </summary>
public info(message: string) {
logger.info(this._sectionName, this._traceId, message);
}
/// <summary> Log message with Warn level. </summary>
public warn(message: string) {
logger.warn(this._sectionName, this._traceId, message);
}
/// <summary> Log message with Error level. </summary>
public err(message: string) {
logger.err(this._sectionName, this._traceId, message);
}
private _traceId: string;
private _sectionName: string;
}

134
lib/builtin-entrypoints.ts Normal file
Просмотреть файл

@ -0,0 +1,134 @@
/////////////////////////////////////////////////////////////////////
// This file defines built-in entrypoints in winery module.
//
import * as fs from 'fs';
import * as vy from './index';
/// <summary> List all application names in current system. </summary>
export function listApplications(request: vy.RequestContext): string[] {
return vy.getApplicationInstanceNames();
}
const DEFAULT_RANK_USER_ENTRYPOINT: number = 700;
/// <summary> List all entry point names under current application. </summary>
export function listEntryPoints(
request: vy.RequestContext,
input: { detail: boolean, allowGlobal: boolean, allowPrivate: boolean }
= { detail: false, allowGlobal: true, allowPrivate: false }
): string[] | vy.objectModel.NamedObjectDefinition[] {
let entryPointDefs: vy.objectModel.NamedObjectDefinition[] = [];
request.application.objectContext.forEach(namedObject => {
let def = namedObject.definition;
if (def.value._type === 'EntryPoint'
&& (input.allowPrivate || !namedObject.definition.private)
&& (input.allowGlobal || namedObject.scope !== "global")) {
entryPointDefs.push(namedObject.definition);
}
});
// Rank entrypoint by displayRank first and then alphabetical order.
entryPointDefs = entryPointDefs.sort((a, b): number => {
let rankA = isNaN(a.value['displayRank']) ? DEFAULT_RANK_USER_ENTRYPOINT : a.value['displayRank'];
let rankB = isNaN(b.value['displayRank']) ? DEFAULT_RANK_USER_ENTRYPOINT : b.value['displayRank'];
if (rankA != rankB) {
return rankA - rankB;
}
// Name should never be equal.
return a.name < b.name ? -1 : 1;
});
return input.detail ? entryPointDefs
: entryPointDefs.map((def) => {
return def.name
});
}
/// <summary> List all named objects under current application. </summary>
export function listNamedObjects(
request: vy.RequestContext,
input: { allowPrivate: boolean, scopes: string[] } = { allowPrivate: false, scopes: ['request', 'application']}): string[] {
let objectNames: string[] = [];
request.application.objectContext.forEach(namedObject => {
if ((input.allowPrivate || !namedObject.definition.private)
&& (namedObject.scope in input.scopes)) {
objectNames.push(namedObject.definition.name);
}
});
return objectNames;
}
/// <summary> Display a named object by name. </summary>
export function getNamedObject(request: vy.RequestContext, input: { name: string }): any {
if (input == null || input.name == null) {
throw new Error("'name' property must be specified under 'input' object of request.");
}
let object = request.getNamedObject(input.name);
if (object == null || object.definition == null) {
return null;
}
return object.definition;
}
/// <summary> List all types supported in current application. </summary>
/// TODO: @dapeng, return types from global and request scope.
export function listTypes(request: vy.RequestContext): string[] {
let appDef = request.application.settings;
let typeNames: string[] = [];
for (let typeDef of appDef.objectContext.types) {
typeNames.push(typeDef.typeName);
}
return typeNames;
}
/// <summary> Get definition of a type in current application. </summary>
/// TODO: @dapeng, return types from global and request scope.
export function getType(request: vy.RequestContext, input: { typeName: string }): vy.objectModel.TypeDefinition {
if (input == null || input.typeName == null) {
throw new Error("'typeName' property must be specified under 'input' object of request.");
}
let appDef = request.application.settings;
let types = appDef.objectContext.types;
for (let i = 0; i < types.length; i++){
if (types[i].typeName.toLowerCase() == input.typeName.toLowerCase()) {
return types[i];
}
}
throw new Error("Type name '" + input.typeName + "' is not supported in current application.");
}
/// <summary> List URI providers supported in current application. </summary>
/// TODO: @dapeng, return providers from global and request scope.
export function listProviders(request: vy.RequestContext): string[] {
let appDef = request.application.settings;
let protocolNames: string[] = [];
for (let providerDef of appDef.objectContext.providers) {
protocolNames.push(providerDef.protocol);
}
return protocolNames;
}
/// <summary> Get the provider definition for a URI protocol. </summary>
/// TODO: @dapeng, return providers from global and request scope.
export function getProvider(request: vy.RequestContext, input: { protocolName: string }): vy.objectModel.ProviderDefinition {
if (input == null || input.protocolName == null) {
throw new Error("'protocolName' property must be specified under 'input' object of request.");
}
let appDef = request.application.settings;
let providers = appDef.objectContext.providers;
for (let provider of providers) {
if (provider.protocol.toLowerCase() === input.protocolName.toLowerCase()) {
return provider;
}
}
throw new Error("Protocol name '" + input.protocolName + "' is not supported in current application.");
}

Просмотреть файл

@ -0,0 +1,87 @@
import * as logger from '@napajs/logger';
import * as metrics from '@napajs/metrics';
import * as app from './app';
import * as wire from './wire';
import * as utils from './utils';
/////////////////////////////////////////////////////////////////
/// Built-in interceptors.
/// <summary> Interceptor: pass through.
/// This interceptor is used for debug purpose when doing per-request override
/// <summary>
export async function passThrough(
context: app.RequestContext): Promise<wire.Response> {
return await context.continueExecution();
}
/// <summary> Interceptor: short circuit.
/// This interceptor is used for debug purpose when doing per-request override
/// <summary>
export async function shortCircuit(
context: app.RequestContext): Promise<wire.Response> {
return Promise.resolve({
responseCode: wire.ResponseCode.Success
});
}
/// <summary> Interceptor: execute entryPoint </summary>
export async function executeEntryPoint(
context: app.RequestContext): Promise<wire.Response> {
let response = await context.continueExecution();
response.output = await utils.makePromiseIfNotAlready(
context.entryPoint(context, context.input));
return response;
}
/// <summary> Interceptor: log request only. </summary>
export async function logRequest(
context: app.RequestContext): Promise<wire.Response> {
logger.debug(JSON.stringify(context.request));
return await context.continueExecution();
}
/// <summary> Interceptor: log response only. </summary>
export async function logResponse(
context: app.RequestContext): Promise<wire.Response> {
let response = await context.continueExecution();
logger.debug(JSON.stringify(response));
return response;
}
/// <summary> Interceptor: log request and response. </summary>
export async function logRequestResponse(
context: app.RequestContext): Promise<wire.Response> {
logger.debug(JSON.stringify(context.request));
let response = await context.continueExecution();
logger.debug(JSON.stringify(response));
return response;
}
/// <summary> Interceptor: finalize response </summary>
export async function finalizeResponse(
context: app.RequestContext): Promise<wire.Response> {
let startTime = metrics.now();
let response = await context.continueExecution();
// Attach debug info if needed.
if (context.controlFlags.debug) {
response.debugInfo = context.debugger.getOutput();
}
// Attach perf info if needed.
if (context.controlFlags.perf) {
response.perfInfo = {
processingLatencyInMS : metrics.elapseSince(startTime)
};
}
return response;
}

97
lib/builtin-types.ts Normal file
Просмотреть файл

@ -0,0 +1,97 @@
import { EntryPoint, Interceptor, RequestContext } from './app';
import * as objectModel from './object-model';
import * as wire from './wire';
import * as utils from './utils';
import * as path from 'path';
////////////////////////////////////////////////////////////////////////
/// JSON definition for built-in object types.
/// <summary> Definition for function object. </summary>
export interface FunctionDefinition {
// <summary> For referencing existing function. </summary>
moduleName?: string,
functionName?: string,
/// <summary> for inline function. </summary>
function?: string;
}
/// <summary> Entrypoint definition. </summary>
export interface EntryPointDefinition extends FunctionDefinition {
/// <summary> _type === 'EntryPoint' </summary>
_type: "EntryPoint",
/// <summary> Optional. Description of entrypoint. </summary>
description?: string,
/// <summary> Optional. Custom execution stack of interceptor names. </summary>
executionStack?: string[],
/// <summary> Optional. Display rank. </summary>
displayRank?: number,
/// <summary> Optional. Example requests. This is for human consumption. </summary>.
exampleRequests?: wire.Request[],
/// <summary> Optional. Example responses. </summary>
exampleResponses?: wire.Response[]
};
/// <summary> Interceptor definition. </summary>
export interface InterceptorDefinition extends FunctionDefinition {
/// <summary> _type === 'Interceptor' </summary>
_type: "Interceptor",
/// <summary> Optional. Description of interceptor </summary>
description?: string,
};
////////////////////////////////////////////////////////////////////////////////
/// Object constructors for built-in objects.
/// <summary> Constructor for Function. </summary>
export function createFunction(
definition: FunctionDefinition,
context: objectModel.ObjectContext): Function {
if (definition.function != null) {
// Dynamicly created function.
// TODO: do security check.
return eval('(' + definition.function + ')');
}
if (definition.moduleName != null && definition.functionName != null) {
// create function from module and function name.
let moduleName = definition.moduleName;
if (moduleName.startsWith('.')) {
moduleName = path.resolve(context.baseDir, moduleName);
}
return utils.appendMessageOnException("Unable to create function '"
+ definition.function
+ "' in module '"
+ definition.moduleName
+ "'.",
() => {
return utils.loadFunction(moduleName, definition.functionName);
});
}
throw new Error("Either property group 'moduleName' and 'functionName' or property 'function' should be present for Function object.");
}
/// <summary> Constructor for EntryPoint. </summary>
export function createEntryPoint(
definition: EntryPointDefinition,
context: objectModel.ObjectContext): EntryPoint {
// TODO: any check?
return <EntryPoint>createFunction(definition, context);
}
/// <summary> Constructor for Interceptor. </summary>
export function createInterceptor(
definition: Interceptor,
context: objectModel.ObjectContext): Interceptor {
// TODO: any check?
return <Interceptor>createFunction(definition, context);
}

5
lib/builtins.ts Normal file
Просмотреть файл

@ -0,0 +1,5 @@
import * as types from './builtin-types';
import * as interceptors from './builtin-interceptors';
import * as entryPoints from './builtin-entrypoints';
export {types, entryPoints, interceptors};

428
lib/config.ts Normal file
Просмотреть файл

@ -0,0 +1,428 @@
import * as path from 'path';
import * as utils from './utils';
import * as objectModel from './object-model';
import * as app from './app';
import * as metrics from '@napajs/metrics'
import * as engine from './engine'
//////////////////////////////////////////////////////////////////////////////////
// Static classes that create or read definition objects.
const SCHEMA_DIR: string = path.resolve(__dirname, '../schema');
/// <summary> Helper class to read TypeDefinition array from config. </summary>
export class ObjectTypeConfig {
/// <summary> JSON schema used to validate conf. </summary>
private static readonly OBJECT_TYPE_CONFIG_SCHEMA: utils.JsonSchema =
new utils.JsonSchema(path.resolve(SCHEMA_DIR, "object-type-config.schema.json"));
/// <summary> Transforms from config object to definition. </summary>
private static _transform: utils.Transform =
new utils.SetDefaultValue({
'override': false
});
/// <summary> Create TypeDefinition array from a JS object array that conform with schema.
/// Throw exception if JS object array doesn't match schema.
/// Schema: "../schema/object-type-config.schema.json"
/// </summary>
/// <param name="jsValue"> a JS object array to create TypeDefinition object. </param>
/// <param name="validateSchema"> Whether validate schema,
/// this option is given due to request object already checked schema at request level. </param>
/// <returns> A list of TypeDefinition objects. </returns>
public static fromConfigObject(jsValue: any[], validateSchema: boolean = true): objectModel.TypeDefinition[] {
if (validateSchema) {
utils.ensureSchema(jsValue, this.OBJECT_TYPE_CONFIG_SCHEMA);
}
jsValue.forEach(obj => {
this._transform.apply(obj);
});
return jsValue;
}
/// <summary> From a config file to create a array of TypeDefinition. </summary>
/// <param name="objectTypeConfig"> TypeDefinition config file. </param>
/// <returns> A list of TypeDefinition objects. </returns>
public static fromConfig(objectTypeConfig: string): objectModel.TypeDefinition[] {
return utils.appendMessageOnException(
"Error found in object type definition file '" + objectTypeConfig + "'.",
() => { return this.fromConfigObject(utils.readConfig(objectTypeConfig)); });
}
}
/// <summary> Helper class to read ProviderDefinition array from config. </summary>
export class ObjectProviderConfig {
/// <summary> JSON schema used to validate conf. </summary>
private static readonly OBJECT_PROVIDER_CONFIG_SCHEMA: utils.JsonSchema =
new utils.JsonSchema(path.resolve(SCHEMA_DIR, "object-provider-config.schema.json"));
/// <summary> Transform from config object to definition. </summary>s
private static _transform: utils.Transform =
new utils.SetDefaultValue({
'override': false
});
/// <summary> Create ProviderDefinition array from a JS value that conform with schema.
/// Throw exception if JS object array doesn't match schema.
/// </summary>
/// <param name="jsValue"> a JS value to create ProviderDefinition object. </param>
/// <param name="validateSchema"> Whether validate schema,
/// this option is given due to request object already checked schema at request level. </param>
/// <returns> A list of ProviderDefinition objects. </returns>
public static fromConfigObject(jsValue: any[], validateSchema: boolean = true): objectModel.ProviderDefinition[]{
if (validateSchema) {
utils.ensureSchema(jsValue, this.OBJECT_PROVIDER_CONFIG_SCHEMA);
}
jsValue.forEach(obj => {
this._transform.apply(obj);
});
return jsValue;
}
/// <summary> Create ProviderDefinition array from a configuration file. (.config or .json)
/// Throw exception if JS object array doesn't match schema.
/// Schema: "../schema/object-provider-config.schema.json"
/// </summary>
/// <param name="objectProviderConfig"> a JSON file in object provider definition schema. </param>
/// <returns> A list of ProviderDefinition objects. </returns>
public static fromConfig(objectProviderConfig: string): objectModel.ProviderDefinition[] {
return utils.appendMessageOnException(
"Error found in object provider definition file '" + objectProviderConfig + "'.",
() => { return this.fromConfigObject(utils.readConfig(objectProviderConfig)); });
}
}
/// <summary> Helper class to read NamedObjectDefinition array from config. </summary>
export class NamedObjectConfig {
/// <summary> JSON schema used to validate conf. </summary>
static readonly NAMED_OBJECT_CONFIG_SCHEMA: utils.JsonSchema =
new utils.JsonSchema(path.resolve(SCHEMA_DIR, "named-object-config.schema.json"));
/// <summary> Transform object from JSON to object. </summary>
private static _transform: utils.Transform =
new utils.SetDefaultValue({
'override': false,
'private': false
});
/// <summary> Create NamedObjectDefinition array from a JS object array that conform with schema.
/// Throw exception if JS object array doesn't match schema.
/// Schema: "../schema/named-object-config.schema.json"
/// </summary>
/// <param name="jsValue"> a JS value array to create NamedObjectDefinition object. </param>
/// <param name="validateSchema"> Whether validate schema,
/// this option is given due to request object already checked schema at request level. </param>
/// <returns> A list of NamedObjectDefinition objects. </returns>
public static fromConfigObject(jsValue: any[], validateSchema: boolean = true): objectModel.NamedObjectDefinition[]{
if (validateSchema) {
utils.ensureSchema(jsValue, this.NAMED_OBJECT_CONFIG_SCHEMA);
}
jsValue.forEach(obj => {
this._transform.apply(obj);
});
return jsValue;
}
/// <summary> Create NamedObjectDefinition array from a configuration file. (.config or .json)
/// Throw exception if configuration file parse failed or doesn't match schema.
/// Schema: "../schema/named-object-config.schema.json"
/// </summary>
/// <param name="namedObjectConfigFile"> a JSON file in named object definition schema. </param>
/// <returns> A list of NamedObjectDefinition objects. </returns>
public static fromConfig(namedObjectConfigFile: string): objectModel.NamedObjectDefinition[] {
return utils.appendMessageOnException(
"Error found in named object definition file '" + namedObjectConfigFile + "'.",
() => { return this.fromConfigObject(utils.readConfig(namedObjectConfigFile)); });
}
}
/// <summary> Helper class to read MetricDefinition array from config. </summary>
export class MetricConfig {
/// <summary> JSON schema used to validate config. </summary>
private static readonly METRIC_CONFIG_SCHEMA: utils.JsonSchema
= new utils.JsonSchema(path.resolve(SCHEMA_DIR, "metric-config.schema.json"));
/// <summary> Transform object from JSON to object. </summary>
private static _transform: utils.Transform =
new utils.SetDefaultValue( {
'dimensionNames': []
}).add(
new utils.TransformPropertyValues({
'type': (metricTypeName: string) => {
let lowerCaseTypeName = metricTypeName.toLowerCase();
switch (lowerCaseTypeName) {
case 'number': return metrics.MetricType.Number;
case 'rate': return metrics.MetricType.Rate;
case 'percentile': return metrics.MetricType.Percentile;
case 'latency': return metrics.MetricType.Percentile;
default: throw new Error("Invalid metric type: '" + metricTypeName + "'.");
}
}
}));
/// <summary> Create MetricDefinition array from a JS object array that conform with schema.
/// Throw exception if JS object array doesn't match schema.
/// Schema: "../schema/metric-config.schema.json"
/// </summary>
/// <param name="sectionName"> Section name used to create counters. </param>
/// <param name="jsValue"> a JS value array to create MetricDefinition object. </param>
/// <returns> A list of NamedObjectDefinition objects. </returns>
public static fromConfigObject(sectionName: string, jsValue: any[]): app.MetricDefinition[] {
utils.ensureSchema(jsValue, this.METRIC_CONFIG_SCHEMA);
jsValue.forEach(obj => {
this._transform.apply(obj);
obj.sectionName = sectionName;
});
return <app.MetricDefinition[]>(jsValue);
}
/// <summary> Create MetricDefinition array from a configuration file. (.config or .JSON)
/// Throw exception if JS object array doesn't match schema.
/// Schema: "../schema/metric-config.schema.json"
/// </summary>
/// <param name="metricConfigFile"> a .config or .JSON file in metric definition schema. </param>
/// <returns> A list of MetricDefinition objects. </returns>
public static fromConfig(sectionName: string, metricConfigFile: string): app.MetricDefinition[] {
return utils.appendMessageOnException(
"Error found in metric definition file '" + metricConfigFile + "'.",
() => { return this.fromConfigObject(sectionName, utils.readConfig(metricConfigFile)); });
}
}
/// <summary> Helper class to read ApplicationSettings from config. </summary>
export class ApplicationConfig {
/// <summary> JSON schema used to validate config. </summary>
private static readonly APP_CONFIG_SCHEMA: utils.JsonSchema
= new utils.JsonSchema(path.resolve(SCHEMA_DIR, "application-config.schema.json"));
/// <summary> Create ApplicationSettings from a JS object that conform with schema.
/// Throw exception if JS object doesn't match schema.
/// Schema: "../schema/application-config.schema.json"
/// </summary>
/// <param name="parentSettings"> Parent settings to inherit as default values. </param>
/// <param name="jsValue"> a JS value to create ApplicationSettings object. </param>
/// <param name="basePath"> Base path used to resolve relative paths. </param>
/// <returns> An ApplicationSettings object. </returns>
public static fromConfigObject(
parentSettings: app.Settings,
jsValue: any,
basePath: string): app.ApplicationSettings {
utils.ensureSchema(jsValue, this.APP_CONFIG_SCHEMA);
let appSettings: app.ApplicationSettings = {
baseDir: basePath,
id: jsValue.id,
description: jsValue.description,
allowPerRequestOverride: jsValue.allowPerRequestOverride,
defaultExecutionStack: jsValue.defaultExecutionStack,
objectContext: parentSettings.objectContext,
metrics: []
};
// Optional: allowPerRequestOverride.
// Inherit engine settings if it's not provided from application.
if (appSettings.allowPerRequestOverride == null) {
appSettings.allowPerRequestOverride = parentSettings.allowPerRequestOverride;
}
// Optional: defaultExecutionStack.
// Inherit engine settings if it's not provided from application.
if (appSettings.defaultExecutionStack == null) {
appSettings.defaultExecutionStack = parentSettings.defaultExecutionStack;
}
// Required: 'objectTypes'
let typeDefFiles: string[] = jsValue.objectTypes;
let typeDefinitions: objectModel.TypeDefinition[] = [];
let typeToFileName: { [typeName: string]: string } = {};
for (let typeDefFile of typeDefFiles) {
let typeDefs = ObjectTypeConfig.fromConfig(path.resolve(basePath, typeDefFile));
for (let typeDefinition of typeDefs) {
if (typeToFileName.hasOwnProperty(typeDefinition.typeName)
&& !typeDefinition.override) {
throw new Error("Object type '"
+ typeDefinition.typeName
+ "' already exists in file '"
+ typeToFileName[typeDefinition.typeName]
+ "'. Did you forget to set property 'override' to true? ");
}
typeDefinitions.push(typeDefinition);
typeToFileName[typeDefinition.typeName] = typeDefFile;
}
}
// Optional: 'objectProviders'
let providerDefFiles: string[] = jsValue.objectProviders;
let providerDefinitions: objectModel.ProviderDefinition[] = [];
let protocolToFileName: { [protocolName: string]: string } = {};
if (providerDefFiles != null) {
for (let providerDefFile of providerDefFiles) {
let providerDefs = ObjectProviderConfig.fromConfig(path.resolve(basePath, providerDefFile));
for (let providerDef of providerDefs) {
if (protocolToFileName.hasOwnProperty(providerDef.protocol)
&& !providerDef.override) {
throw new Error("Object provider with protocol '"
+ providerDef.protocol
+ "' already exists in file '"
+ protocolToFileName[providerDef.protocol]
+ "' .Did you forget to set property 'override' to true? ");
}
providerDefinitions.push(providerDef);
protocolToFileName[providerDef.protocol] = providerDefFile;
}
}
}
// Required: 'namedObjects'
let namedObjectDefFiles: string[] = jsValue.namedObjects;
let namedObjectDefinitions: objectModel.NamedObjectDefinition[] = [];
let nameToFileName: {[objectName: string]: string} = {};
for (let namedObjectDefFile of namedObjectDefFiles) {
let objectDefs = NamedObjectConfig.fromConfig(path.resolve(basePath, namedObjectDefFile));
for (let objectDef of objectDefs) {
if (nameToFileName.hasOwnProperty(objectDef.name)
&& !objectDef.override) {
throw new Error("Named object'"
+ objectDef.name
+ "' already exists in file '"
+ nameToFileName[objectDef.name]
+ "'. Did you forget to set property 'override' to true? ");
}
namedObjectDefinitions.push(objectDef);
nameToFileName[objectDef.name] = namedObjectDefFile;
}
}
appSettings.objectContext = new objectModel.ScopedObjectContextDefinition(
parentSettings.objectContext,
typeDefinitions,
providerDefinitions,
namedObjectDefinitions,
true // Enable depenency check.
);
// Optional: 'metrics'
let metricDefObject: any = jsValue.metrics;
if (metricDefObject != null) {
let sectionName = metricDefObject.sectionName;
let metricDefFiles: string[] = metricDefObject.definition;
let metricToFilename: { [metricName: string]: string } = {}
metricDefFiles.forEach(metricDefFile => {
let metricDefs = MetricConfig.fromConfig(
sectionName,
path.resolve(basePath, metricDefFile));
metricDefs.forEach(metricDef => {
if (metricToFilename.hasOwnProperty(metricDef.name)) {
throw new Error("Metric '"
+ metricDef.name
+ "' already defined in file '"
+ metricToFilename[metricDef.name]
+ "'.");
}
appSettings.metrics.push(metricDef);
metricToFilename[metricDef.name] = metricDefFile;
});
});
}
return appSettings;
}
/// <summary> Create ApplicationSettings object from application config file (.config or .json)
/// Throws exception if configuration file parse failed or doesn't match the schema.
/// Schema: '../schema/application-config.schema.json'
/// </summary>
/// <param name="parentSettings"> Parent settings to inherit. </param>
/// <param name="appConfigFile"> a JSON file in application config schema. </param>
public static fromConfig(
parentSettings: app.Settings,
appConfigFile: string): app.ApplicationSettings {
return utils.appendMessageOnException(
"Error found in application definition file '" + appConfigFile + "'.",
() => {
return this.fromConfigObject(
parentSettings,
utils.readConfig(appConfigFile), path.dirname(appConfigFile));
});
}
}
/// <summary> Helper class to read EngineSettings from config. </summary>
export class EngineConfig {
/// <summary> JSON schema used to validate config. </summary>
private static readonly SETTINGS_SCHEMA: utils.JsonSchema
= new utils.JsonSchema(path.resolve(SCHEMA_DIR, "engine-config.schema.json"));
/// <summary> Create EngineSettings from a JS object that conform with schema.
/// Throw exception if JS object doesn't match schema.
/// Schema: "../schema/engine-config.schema.json"
/// </summary>
/// <param name="jsValue"> a JS value to create EngineSettings object. </param>
/// <param name="basePath"> Base path used to resolve relative paths. </param>
/// <returns> An EngineSettings object. </returns>
public static fromConfigObject(jsValue: any, basePath: string): engine.EngineSettings {
utils.ensureSchema(jsValue, this.SETTINGS_SCHEMA);
let typeDefinitions: objectModel.TypeDefinition[] = [];
if (jsValue.objectTypes != null) {
for (let fileName of <string[]>(jsValue.objectTypes)) {
let filePath = path.resolve(basePath, fileName);
typeDefinitions = typeDefinitions.concat(ObjectTypeConfig.fromConfig(filePath));
}
}
let providerDefinitions: objectModel.ProviderDefinition[] = [];
if (jsValue.objectProviders != null) {
for (let fileName of <string[]>(jsValue.objectProviders)) {
let filePath = path.resolve(basePath, fileName);
providerDefinitions = providerDefinitions.concat(ObjectProviderConfig.fromConfig(filePath));
}
}
let namedObjectDefinitions: objectModel.NamedObjectDefinition[] = [];
if (jsValue.namedObjects != null ){
for (let fileName of <string[]>(jsValue.namedObjects)) {
let filePath = path.resolve(basePath, fileName);
namedObjectDefinitions = namedObjectDefinitions.concat(NamedObjectConfig.fromConfig(filePath));
}
}
return {
baseDir: basePath,
allowPerRequestOverride: jsValue.allowPerRequestOverride,
throwExceptionOnError: jsValue.throwExceptionOnError,
defaultExecutionStack: jsValue.defaultExecutionStack,
objectContext: new objectModel.ScopedObjectContextDefinition(
null,
typeDefinitions,
providerDefinitions,
namedObjectDefinitions,
true)
};
}
/// <summary> Create EngineSettings object from engine config file (.config or .json)
/// Throws exception if configuration file parse failed or doesn't match the schema.
/// Schema: '../schema/engine-config.schema.json'
/// </summary>
/// <param name="engineConfigFile"> a JSON file in engine config schema. </param>
/// <returns> An EngineSettings object. </returns>
public static fromConfig(engineConfigFile: string): engine.EngineSettings {
return utils.appendMessageOnException(
"Error found in winery setting file: '" +engineConfigFile + "'.",
() => {
return this.fromConfigObject(
utils.readConfig(engineConfigFile),
path.dirname(engineConfigFile));
});
}
}

271
lib/engine.ts Normal file
Просмотреть файл

@ -0,0 +1,271 @@
import * as napa from 'napajs';
import * as path from 'path';
import * as assert from 'assert';
import { Settings, Application, RequestContext} from './app';
import * as objectContext from './object-context';
import * as wire from './wire';
import * as config from './config';
import * as utils from './utils';
/// <summary> Engine config, which is the root config of winery. </summary>
export interface EngineSettings extends Settings{
/// <summary> Throw exception on error, or return Response with error code. Default is true. </summary>
throwExceptionOnError: boolean;
}
/// <summary> Interface for application engine. </summary>
export interface Engine {
/// <summary> Register an application instance in current engine. </summary>
/// <param name="appModuleName"> module name of a winery application.</param>
/// <param name="appInstanceNames"> a list of strings used as names of application instances.</param>
/// <param name="zone"> zone to run the app. If undefined, use current isolate. </param>
register(appModuleName: string, appInstanceNames: string[], zone: napa.Zone): void;
/// <summary> Serve a request. </summary>
/// <param name="request"> A JSON string or a request object. </param>
serve(request: string | wire.Request): Promise<wire.Response>;
/// <summary> Get application instance names served by this engine. </param>
applicationInstanceNames: string[];
}
/// <summary> Engine on local container or Node.JS isolate. </summary>
export class LocalEngine implements Engine{
// Lower-case name to application map.
private _applications: Map<string, Application> = new Map<string, Application>();
// Enabled application names.
private _applicationInstanceNames: string[] = [];
// Engine settings.
private _settings: EngineSettings;
// Global scope object context.
private _objectContext: objectContext.ScopedObjectContext;
/// <summary> Constructor. </summary>
/// <param> winery engine settings. </summary>
public constructor(settings: EngineSettings = null) {
this._settings = settings;
this._objectContext = new objectContext.ScopedObjectContext(
"global",
this._settings.baseDir,
null,
settings.objectContext
);
}
/// <summary> Register an application instance in current engine. </summary>
/// <param name="appModuleName"> module name of a winery application.</param>
/// <param name="appInstanceNames"> a list of strings used as names of application instances.</param>
/// <param name="zone"> zone to run the app. If undefined, use current isolate. </param>
public register(
appModuleName: string,
appInstanceNames: string[],
zone: napa.Zone = null): void {
if (zone != null) {
throw new Error("LocalEngine doesn't support register on a different Zone.");
}
// Load application.
let appConfigPath = require.resolve(appModuleName + '/app.json');
let app = new Application(
this._objectContext,
config.ApplicationConfig.fromConfig(
this._settings,
appConfigPath));
// Put application in registry.
for (let instanceName of appInstanceNames) {
let lowerCaseName = instanceName.toLowerCase();
if (this._applications.has(lowerCaseName)) {
throw new Error("Already registered with application name: '`$applicationName`'.");
}
this._applications.set(lowerCaseName, app);
this._applicationInstanceNames.push(instanceName);
}
}
/// <summary> Serve a request. </summary>
/// <param name="request"> A JSON string or a request object. </param>
public serve(request: string | wire.Request): Promise<wire.Response> {
return new Promise<RequestContext>(resolve => {
if (typeof request === 'string') {
request = utils.appendMessageOnException(
". Fail to parse request string.",
() => { return JSON.parse(<string>request);});
}
let appName = (<wire.Request>request).application;
if (appName == null) {
throw new Error("Property 'application' is missing from request.");
}
resolve(new RequestContext(
this.getApplication(appName),
<wire.Request>request));
}).then((context: RequestContext) => {
return context.execute();
});
}
/// <summary> Get application names. </summary>
public get applicationInstanceNames(): string[] {
return this._applicationInstanceNames;
}
/// <summary> Get engine level object context. </summary>
public get objectContext(): objectContext.ScopedObjectContext {
return this._objectContext;
}
/// <summary> Get application by name. </summary>
public getApplication(appName: string): Application {
let loweredName = appName.toLowerCase();
if (this._applications.has(loweredName)) {
return this._applications.get(loweredName);
}
throw new Error("'" + appName + "' is not a known application");
}
/// <summary> Get global settings. </summary>
public get settings(): Settings {
return this._settings;
}
}
/// <summary> Engine on a remote NapaJS container. </summary>
export class RemoteEngine {
/// <summary> Zone to run the app </summary>
private _zone: napa.Zone;
/// <summary> Application instance names running on this engine. </summary>
private _applicationInstanceNames: string[] = [];
/// <summary> Constructor. </summary>
public constructor(zone: napa.Zone) {
assert(zone != null);
this._zone = zone;
}
/// <summary> Register an application instance in current engine. </summary>
/// <param name="appModuleName"> module name of a winery application.</param>
/// <param name="appInstanceNames"> a list of strings used as names of application instances.</param>
/// <param name="zone"> zone to run the app. If undefined, use current isolate. </param>
public register(appModuleName: string,
appInstanceNames: string[],
zone: napa.Zone = undefined): void {
if (zone != null && zone != this._zone) {
throw new Error("RemoteEngine cannot register application for a different zone.");
}
// TODO: @dapeng. implement this after introducing container.runAllSync.
// this._container.loadFileSync(path.resolve(__dirname, "index.js"));
// this._container.runAllSync('register', [appModuleName, JSON.stringify(appInstanceNames)]);
// this._applicationInstanceNames = this._applicationInstanceNames.concat(appInstanceNames);
}
/// <summary> Serve a request. </summary>
/// <param name="request"> A JSON string or a request object. </param>
public async serve(request: string | wire.Request): Promise<wire.Response> {
//let responseString = this._container.run('serve', [JSON.stringify(request)], );
let zone = this._zone;
return zone.execute('winery', 'serve', [request])
.then((result: string) => {
return Promise.resolve(wire.ResponseHelper.parse(result));
});
}
/// <summary> Get application instance names served by this engine. </param>
public get applicationInstanceNames(): string[] {
return this._applicationInstanceNames;
}
}
/// <summary> Engine hub. (this can only exist in Node.JS isolate) </summary>
export class EngineHub implements Engine {
/// <summary> Local engine. Only instantiate when application is registered locally. </summary>
private _localEngine: Engine;
/// <summary> Zone to remote engine map. </summary>
private _remoteEngines: Map<napa.Zone, Engine> = new Map<napa.Zone, Engine>();
/// <summary> Settings for local engine if needed. </summary>
private _settings: EngineSettings;
/// <summary> Application instance names. </summary>
private _applicationInstanceNames: string[] = [];
/// <summary> Application instance name to engine map. </summary>
private _engineMap: Map<string, Engine> = new Map<string, Engine>();
/// <summary> Constructor. </summary>
/// <param> winery engine settings. </summary>
public constructor(settings: EngineSettings = null) {
this._settings = settings;
}
/// <summary> Register an application for serving. </summary>
/// <param name="moduleName"> module name of a winery application.</param>
/// <param name="applicationNames"> a list of strings used as application names</param>
/// <param name="zone"> zone to run the app. If null, use current isolate. </param>
public register(appModuleName: string, appInstanceNames: string[], zone: napa.Zone = undefined) {
let engine: Engine = undefined;
if (zone == null) {
if (this._localEngine == null) {
this._localEngine = new LocalEngine(this._settings);
}
engine = this._localEngine;
}
else {
if (this._remoteEngines.has(zone)) {
engine = this._remoteEngines.get(zone);
}
else {
engine = new RemoteEngine(zone);
this._remoteEngines.set(zone, engine);
}
}
engine.register(appModuleName, appInstanceNames, undefined);
this._applicationInstanceNames = this._applicationInstanceNames.concat(appInstanceNames);
for (let instanceName of this._applicationInstanceNames) {
let lowerCaseName = instanceName.toLowerCase();
this._engineMap.set(lowerCaseName, engine);
}
}
/// <summary> Serve winery request. </summary>
public async serve(request: string | wire.Request): Promise<wire.Response> {
return new Promise<Engine>(resolve => {
if (typeof request === 'string') {
request = utils.appendMessageOnException(
". Fail to parse request string.",
() => { return JSON.parse(<string>request);});
}
// TODO: @dapeng, avoid extra parsing/serialization for remote engine.
let appName = (<wire.Request>request).application;
if (appName == null) {
throw new Error("Property 'application' is missing from request.");
}
let lowerCaseName = appName.toLowerCase();
if (!this._engineMap.has(lowerCaseName)) {
throw new Error("Application '" + appName + "' is not registered for serving");
}
resolve(this._engineMap.get(lowerCaseName));
}).then((engine: Engine) => {
return engine.serve(request);
});
}
/// <summary> Get application instance names. </summary>
public get applicationInstanceNames(): string[] {
return this._applicationInstanceNames;
}
}

67
lib/index.ts Normal file
Просмотреть файл

@ -0,0 +1,67 @@
// Export user types to a flattened namespace.
import * as objectModel from './object-model';
import * as builtins from './builtins';
import * as config from './config';
import * as utils from './utils'
import * as engine from './engine';
export {builtins, config, engine, objectModel, utils};
export * from './app'
export * from './wire'
import { Request, Response} from './wire';
import * as path from 'path';
import * as napa from 'napajs';
/// <summary> A global engine instance. </summary>
let _engine: engine.Engine = undefined;
let _engineSettings = config.EngineConfig.fromConfig(
path.resolve(__dirname, "../config/engine.json"));
declare var __in_napa: boolean;
/// <summary> Initialize engine on demand. </summary>
function initEngine() {
if (typeof __in_napa !== undefined) {
// This module is loaded in NapaJS container.
_engine = new engine.LocalEngine(_engineSettings);
}
else {
// This module is loaded in Node.JS isolate.
_engine = new engine.EngineHub(_engineSettings);
}
}
/// <summary> Register a winery application. </summary>
/// <param name="appModuleName"> Module name for winery application, which contains an app.json under the path. </param>
/// <param name="appInstanceNames"> A list of names used to serve application, which correspond to 'application' property in Request. </param>
/// <param name="zone"> Optional. Napa zone to run the application, if not specified, run application in current V8 isolate. </param>
/// <exception> Throws Error if the module is not found or not a valid winery application. </exception>
export function register(
appModuleName: string,
appInstanceNames: string[],
zone: napa.Zone = null): void {
// Lazy creation of engine when register is called at the first time.
if (_engine == null) {
initEngine();
}
return _engine.register(
appModuleName,
appInstanceNames,
zone);
}
/// <summary> Serve a request with a promise of response. </summary>
/// <param name="request"> Requet in form of a JSON string or Request object. </param>
/// <returns> A promise of Response. This function call may be synchrous or asynchrous depending on the entrypoint. </returns>
export async function serve(
request: string | Request): Promise<Response> {
return await _engine.serve(request);
}
/// <summary> Get all application names served by current engine. </summary>
export function getApplicationInstanceNames(): string[] {
return _engine.applicationInstanceNames;
}

147
lib/named-object.ts Normal file
Просмотреть файл

@ -0,0 +1,147 @@
import { ObjectContext } from './object-context';
///////////////////////////////////////////////////////////////////////////////
// Interfaces for Named Objects.
// Named Objects are application-level objects with a well-known name, so user can retrive the object via app.getObject('<name>');
//
// There are two types of named objects.
// 1) Named objects provided from JSON file per application, whose lifecycle is process- level.
// 2) Named objects provided from a single Napa request, whose lifecycle is during the request.
/// <summary> Class for named object that holds a definition and value.
/// Definition is needed to construct this named object under a different ObjectContext, e.g, from request.
/// </summary>
/// <summary> Interface for Named object definition. </summary>
export interface NamedObjectDefinition {
name: string;
description?: string;
private?: boolean;
override?: boolean;
value: any;
/// <summary> Dependency from current definition to object context. This is calculated automatically.</summary>
dependencies?: ObjectContextDependency;
}
export interface NamedObject {
/// <summary> Definition of current named object. </summary>
definition: NamedObjectDefinition;
/// <summary> Value of current named object </summary>
value: any;
/// <summary> Scope of where this named object is provided. </summary>
readonly scope: string;
}
/// <summary> Interface for Named Object collection. </summary>
export interface NamedObjectCollection {
/// <summary> Get named object by name. </summary>
/// <param name="name"> Name. Case-sensitive. </summary>
/// <returns> Named object if found. Otherwise undefined. </returns>
get(name: string): NamedObject;
/// <summary> Iterator each object in this collection. </summary>
forEach(callback: (object: NamedObject) => void): void;
}
/// <summary> An implementation of NamedObjectCollection based on name to object registry. </summary>
export class NamedObjectRegistry implements NamedObjectCollection {
/// <summary> Name to object map. Case sensitive. </summary>
private _nameToObjectMap: Map<string, NamedObject> = new Map<string, NamedObject>();
/// <summary> Get object by name. </summary>
/// <param name="name"> Case sensitive name. </param>
/// <returns> undefined if not present, otherwise an instance of NamedObject. </returns>
public get(name: string): NamedObject {
return this._nameToObjectMap.get(name);
}
/// <summary> Tell if a name exists in this registry. </summary>
/// <returns> True if exists, otherwise false. </returns>
public has(name: string): boolean {
return this._nameToObjectMap.has(name);
}
/// <summary> Iterate each object in this registry. </summary>
public forEach(callback: (object: NamedObject) => void): void {
this._nameToObjectMap.forEach(object => {
callback(object);
});
}
/// <summary> Insert a named object. </summary>
/// <param name="object"> an Named object instance. </param>
public insert(object: NamedObject): void {
this._nameToObjectMap.set(object.definition.name, object);
}
/// <summary> Create NamedObjectRegistry from a collection of NamedObjectDefinition objects. </summary>
/// <param name="scope"> Scope that current object definition apply to. Can be 'global', 'application', 'request', etc. </summary>
/// <param name="namedObjectDefCollection"> Collection of NamedObjectDefinition objects. </param>
/// <param name="context"> A list of ObjectContext objects. </param>
/// <returns> NamedObjectRegistry </returns>
public static fromDefinition(
scope: string,
namedObjectDefCollection: NamedObjectDefinition[],
context: ObjectContext): NamedObjectRegistry {
let registry = new NamedObjectRegistry();
if (namedObjectDefCollection != null) {
for (let def of namedObjectDefCollection) {
let value = context.create(def.value);
registry.insert({
definition: def,
value: value,
scope: scope
});
}
}
return registry;
}
}
/// <summary> Dependency information on types, object and providers.
/// When type, object, provider override happens at request time,
/// We use this information to determine if a named object needs to be invalidated at request time.
/// We only analyze dependency information for named objects registered at application level,
/// as request level named object anyway will be re-created.
/// </summary>
export class ObjectContextDependency {
private _dependentTypesNames: Set<string> = new Set<string>();
private _dependentObjectNames: Set<string> = new Set<string>();
private _dependentProtocolNames: Set<string> = new Set<string>();
/// <summary> Set a depenency on a object type </summary>
public setTypeDependency(typeName: string) {
this._dependentTypesNames.add(typeName);
}
/// <summary> Set a depenency on a URI protocol. </summary>
public setProtocolDependency(protocolName: string) {
this._dependentProtocolNames.add(protocolName);
}
/// <summary> Set a depenency on a named object. </summary>
public setObjectDependency(objectName: string) {
this._dependentObjectNames.add(objectName);
}
/// <summary> Get all dependent type names. </summary>
public get typeDependencies(): Set<string> {
return this._dependentTypesNames;
}
/// <summary> Get all dependent URI protocol names. </summary>
public get protocolDependencies(): Set<string> {
return this._dependentProtocolNames;
}
/// <summary> Get all dependent object names. </summary>
public get objectDependencies(): Set<string> {
return this._dependentObjectNames;
}
};

522
lib/object-context.ts Normal file
Просмотреть файл

@ -0,0 +1,522 @@
import * as path from 'path';
import { TypeDefinition, TypeRegistry, ObjectFactory } from './object-type';
import { Uri, ProviderDefinition, ObjectProvider, ProviderRegistry } from './object-provider';
import { NamedObjectDefinition, NamedObject, NamedObjectRegistry, ObjectContextDependency } from './named-object';
///////////////////////////////////////////////////////////////////////////////////////////////
/// This file defines the interfaces and an implementation of Object Context.
/// Object Context is the interface for object creation and named objects accessing.
///
/// Object context is introduced for constructing/providing an object and getting a named object under a request or application context.
/// It's necessary for object factories and providers to construct/provide objects with fields that point to some relational objets.
/// The type creator might know how to construct its own properties. But sometimes there are
/// references in object of this type pointing to another type.
///
/// e.g: {
/// "_type": "WorkGroup",
/// "name": "Some work group",
/// "members": [
/// "people:/a",
/// "people:/b"
/// ]
/// }
/// or
/// {
/// "_type" : "WorkGroup",
/// "name": "Some work group",
/// "members" : [
/// {"_type" : "People", "name": "Alex", "alias": "a"},
/// {"_type" : "People", "name": "Brian", "alias": "b"},
/// ]
/// }
///
/// In this case, constructor WorkGroup doesn't know how to construct People objects from URI or need to access constructor of type "People".
///
/// Another case is that objects might want to access named objects.
///
/// e.g: {
/// "_type": "Number"
/// "expression": "${web.maxPassagePerDoc} + 1"
/// }
///
/// In this case, the object want to consume a named object "web.maxPassagePerDoc" under current context.
///
/// Object context is introduced for helping these cases.
/// </summary>
/// <summary> Interface for ObjectContext </summary>
export interface ObjectContext {
/// <summary> Create an object from input. </summary>
/// <param name="input"> Input JS value. </param>
/// <returns> Created object or null if failed. </returns>
create(input: any): any;
/// <summary> Get an named object from current context. </summary>
/// <param name="name"> Name. case-sensitive. </param>
/// <returns> Named object, or undefined if not found. </returns>
get(name: string): NamedObject;
/// <summary> Iterator each object on current context. Overrided object will only be visited once from higher scope. </summary>
/// <param name="callback"> Callback on each named object. </summary>
forEach(callback: (object: NamedObject) => void): void;
/// <summary> Return the base directory of this object context.
/// User can use this directory to resolve files with relative paths.
/// </summary>
baseDir: string;
}
/// <summary> Scoped object context is an implementation of ObjectContext
/// Which holds object factory, object provider and named object collection at current scope.
/// The scoped object context also has a pointer to a larger scope context, when objects
/// request cannot be handled at current scope, it will redirect to larger scope.
/// The chaining nature of scoped object context can be depicted as following:
/// +-------------------+ +-----------------------+ +----------------------+
/// | Request scope | parent | Application | parent | Global |
/// | ObjectContext |------->| Scope ObjectContext |-------> | Scope ObjectContext |
/// +-------------------+ +-----------------------+ +----------------------+
export class ScopedObjectContext implements ObjectContext {
private _scope: string;
private _parent: ScopedObjectContext;
private _definition: ScopedObjectContextDefinition;
private _objectFactory: ObjectFactory;
private _objectProvider: ObjectProvider;
private _namedObjects: NamedObjectRegistry;
// Base directory used to resolve path.
private _baseDir: string;
/// <summary> Constructor. Throws exception if there is unrecongized type, protocol or cyclic named object dependency. </summary>
/// <param name="scopeName"> Scope name that current object context applies to. Can be 'global', 'application', 'request'. </param>
/// <param name="parent"> Parent object context if exists. Application scope has parent scope as null. </param>
/// <param name="definition"> Definition for current object context. </param>
/// <param name="baseDir"> Base directory used to resolve relative file names. </param>
public constructor(
scopeName: string,
baseDir: string,
parent: ScopedObjectContext,
definition: ScopedObjectContextDefinition) {
this._scope = scopeName;
this._baseDir = baseDir;
this._definition = definition;
this._parent = parent;
this._objectFactory = TypeRegistry.fromDefinition(definition.types, baseDir);
this._objectProvider = ProviderRegistry.fromDefinition(definition.providers, baseDir);
this._namedObjects = NamedObjectRegistry.fromDefinition(scopeName, definition.namedObjects, this);
}
/// <summary> Get scope name. </summary>
public get scope(): string {
return this._scope;
}
/// <summary> Get base directory. </summary>
public get baseDir(): string {
return this._baseDir;
}
/// <summary> Get definition for current object context. </summary>
public get definition(): ScopedObjectContextDefinition {
return this._definition;
}
/// <summary> Get parent context. </summary>
public get parent(): ScopedObjectContext {
return this._parent;
}
/// <summary> Iterator each object on current context. Overrided object will only be visited once from higher scope. </summary>
/// <param name="callback"> Callback on each named object. </summary>
public forEach(callback: (object: NamedObject) => void): void {
let visited = new Set<string>();
let currentScope: ScopedObjectContext = this;
while (currentScope != null) {
currentScope._namedObjects.forEach(object => {
if (!visited.has(object.definition.name)) {
visited.add(object.definition.name);
callback(object);
}
});
currentScope = currentScope._parent;
}
}
/// <summary> Create JS value from an JS input.
/// Exception is thrown when creation failed.
/// </summary>
/// <param name="input"> Any JS value as input </param>
/// <returns> JS value constructed from input. </param>
public create(input: any): any {
if (Array.isArray(input)) {
if (input.length == 0) {
return input;
}
if (typeof input[0] == 'string') {
let uris: Uri[] = [];
let success = input.every((uri: string) => {
let ret = Uri.tryParse(uri);
if (ret.success) {
uris.push(ret.uri);
}
return ret.success;
});
if (!success) {
// If any of string is not URI, return itself.
return input;
}
let provider = this.selectProvider(uris[0], input);
return provider.provide(uris, this);
}
else if (typeof input[0] === 'object') {
if (input[0].hasOwnProperty('_type')) {
let typeName = input[0]['_type'];
let factory = this.selectFactory(typeName, input);
return factory.create(input, this);
}
}
return input;
}
else if (typeof input === 'string') {
let ret = Uri.tryParse(input);
if (ret.success) {
// Input is URI, select object provider from current scope to ancesters.
let uri = ret.uri;
let provider = this.selectProvider(uri, input);
return provider.provide(uri, this);
}
else {
// If a string is not URI, return itself.
return input;
}
}
else if (typeof input === 'object') {
// Object with type.
if (input.hasOwnProperty('_type')) {
let typeName = input['_type'];
let factory = this.selectFactory(typeName, input);
return factory.create(input, this);
}
}
return input;
}
/// <summary> Get an named object from current context </summary>
/// <param name="name"> Name of the object. Case-sensitive. </param>
/// <returns> NamedObject if found, otherwise undefined. </returns>
public get(name: string): NamedObject {
// We only support 2 level of scopes for now (app + per request)
let namedObject = this._namedObjects.get(name);
if (namedObject != null) {
return namedObject;
}
// Not found at current level, try to find in parent.
if (this._parent != null) {
namedObject = this._parent._namedObjects.get(name);
if (namedObject != null) {
// We check if the named object returned from parent scope needs
// to be invalidated in current scope. In that case we will re-create
// it and insert into cache in current scope.
if (this.needsUpdate(namedObject)) {
namedObject = {
definition: namedObject.definition,
value: this.create(namedObject.definition.value),
scope: this._scope
};
this._namedObjects.insert(namedObject);
}
}
}
return namedObject;
}
/// <summary> Determine if a named object is sensitive to type/provider/named object override in current context,
/// If yes, the object need to be updated.
/// </summary>
public needsUpdate(namedObject: NamedObject): boolean {
if (this._parent == null) {
// Current scope is top scope, override never happens.
return false;
}
let def = namedObject.definition;
// Object context override from request happened.
let overrides = this._definition;
if (this._definition.types.length != 0) {
let typeDeps = def.dependencies.typeDependencies;
typeDeps.forEach(typeDep => {
// Type override happened.
if (overrides.getType(typeDep) != null) {
return true;
}
});
}
if (this._definition.providers.length != 0) {
let providerDeps = def.dependencies.protocolDependencies;
providerDeps.forEach(providerDep => {
// Provide override happened.
if (overrides.getProvider(providerDep) != null) {
return true;
}
});
}
if (this._definition.namedObjects.length != 0) {
let objectDeps = def.dependencies.objectDependencies;
objectDeps.forEach(objectDep => {
// Dependent named object override happened.
if (overrides.getNamedObject(objectDep) != null) {
return true;
}
});
}
return false;
}
/// <summary> Select object provider from current scope to ancesters. </summary>
/// <param name='uri'> URI object. </param>
/// <param name="input"> Any JS value as input. </param>
private selectProvider(uri: Uri, input: any): ObjectProvider {
if (uri != null) {
for (let scope: ScopedObjectContext = this; scope != null; scope = scope._parent) {
let provider = scope._objectProvider;
if (provider != null && provider.supports(uri.protocol)) {
return provider;
}
}
}
throw new Error("Cannot create object, URI protocol '"
+ uri.protocol
+ "' is not supported. Input="
+ JSON.stringify(input));
}
/// <summary> Select object factory from current scope to ancesters. </summary>
/// <param name='typeName'> Object type. </param>
/// <param name="input"> Any JS value as input. </param>
private selectFactory(typeName: string, input: any): ObjectFactory {
for (let scope: ScopedObjectContext = this; scope != null; scope = scope._parent) {
let factory = scope._objectFactory;
if (factory != null && factory.supports(typeName)) {
return factory;
}
}
throw new Error("Cannot create object, _type '"
+ typeName
+ "' is not supported. Input="
+ JSON.stringify(input));
}
}
/// <summary> Class for scoped object context definition. </summary>
export class ScopedObjectContextDefinition {
/// <summary> Parent scoped object definition which is necessary to analze dependencies. </summary>
private _parent: ScopedObjectContextDefinition;
private _typeDefinitions: TypeDefinition[];
private _typeNameToDefinition: Map<string, TypeDefinition>;
private _providerDefinitions: ProviderDefinition[];
private _protocolNameToDefinition: Map<string, ProviderDefinition>;
private _objectDefinitions: NamedObjectDefinition[];
private _objectNameToDefinition: Map<string, NamedObjectDefinition>;
/// <summary> Constructor </summary>
/// <param name="parentDefinition"> Definition for parent scope. Set to null if there is not parent scope. </param>
/// <param name="typeDefs"> Object type definitions. </param>
/// <param name="providerDefs"> Object provider definitions. </param>
/// <param name="objectDefs"> Named object definitions. </param>
/// <param name="enableDependencyAnalysis"> Whether enable dependency analysis on this context.
/// Currently we do for 'global' and 'application' scope, but not 'request' scope. </param>
public constructor(
parentDefinition: ScopedObjectContextDefinition,
typeDefs: TypeDefinition[],
providerDefs: ProviderDefinition[],
objectDefs: NamedObjectDefinition[],
enableDependencyAnalysis: boolean) {
this._parent = parentDefinition;
this._typeDefinitions = typeDefs;
this._providerDefinitions = providerDefs;
this._objectDefinitions = objectDefs;
this._typeNameToDefinition = new Map<string, TypeDefinition>();
for (let def of typeDefs) {
this._typeNameToDefinition.set(def.typeName, def);
}
this._protocolNameToDefinition = new Map<string, ProviderDefinition>();
for (let def of providerDefs) {
this._protocolNameToDefinition.set(def.protocol, def);
}
this._objectNameToDefinition = new Map<string, NamedObjectDefinition>();
for (let def of objectDefs) {
this._objectNameToDefinition.set(def.name, def);
}
if (enableDependencyAnalysis) {
this.analyzeNamedObjectDependencies();
}
}
/// <summary> Parent scope context definition. </summary>
public get parent(): ScopedObjectContextDefinition {
return this._parent;
}
/// <summary> Get all type definition in current context. </summary>
public get types(): TypeDefinition[] {
return this._typeDefinitions;
}
/// <summary> Get type definition by type name. </summary>
public getType(typeName: string): TypeDefinition {
let def = this._typeNameToDefinition.get(typeName);
if (def == null && this._parent != null) {
def = this._parent.getType(typeName);
}
return def;
}
/// <summary> Get all provider definition in current context. </summary>
public get providers(): ProviderDefinition[] {
return this._providerDefinitions;
}
/// <summary> Get provider definition by protocol name. </summary>
public getProvider(protocolName: string): ProviderDefinition {
let def = this._protocolNameToDefinition.get(protocolName);
if (def == null && this._parent != null) {
return this._parent.getProvider(protocolName);
}
return def;
}
/// <summary> Get all named object definition in current context. </summary>
public get namedObjects(): NamedObjectDefinition[] {
return this._objectDefinitions;
}
/// <summary> Get named object definition by name. </summary>
public getNamedObject(name: string): NamedObjectDefinition {
let def = this._objectNameToDefinition.get(name);
if (def == null && this._parent != null) {
return this._parent.getNamedObject(name);
}
return def;
}
/// <summary> Analyze named objects dependencies against type definition, provider definition and other named objects in current context.
/// After this call, contextDependency member of elements in 'defs' will be filled.
/// Exception will be thrown if there are unrecoginized types/URI protocols or cyclic dependencies.
/// </summary>
private analyzeNamedObjectDependencies(): void {
// First pass to analyze direct dependencies, do type check and protocol check.
for (let def of this._objectDefinitions) {
def.dependencies = new ObjectContextDependency();
ScopedObjectContextDefinition.analyzeDirectDependencies(def.dependencies, def.value);
// Do type check.
let typeDeps = def.dependencies.typeDependencies;
typeDeps.forEach(typeDep => {
if (this.getType(typeDep) == null) {
throw new Error("Unrecoginized type '" + typeDep + "' found in named object '" + def.name + "'.");
}
});
// Do URI provider check.
let protocolDeps = def.dependencies.protocolDependencies;
protocolDeps.forEach(protocolDep => {
if (this.getProvider(protocolDep) == null) {
throw new Error("Unrecongized URI protocol '" + protocolDep + "' found in named object '" + def.name + "'.");
}
});
}
// Second pass to get closure from redirect/indirect dependencies.
// Key: object name. Value: Closure of dependent object names.
let resolved: Map<string, Set<string>> = new Map<string, Set<string>>();
let toResolve: { unresolvedDeps: Set<string>, definition: NamedObjectDefinition }[] = [];
// Create dependencies to resolve.
for (let def of this._objectDefinitions) {
let objectDeps = def.dependencies.objectDependencies;
if (objectDeps.size != 0) {
let unresolvedDeps = new Set<string>();
objectDeps.forEach(depName => {
unresolvedDeps.add(depName);
});
toResolve.push({
unresolvedDeps: unresolvedDeps,
definition: def
});
}
else {
resolved.set(def.name, objectDeps);
}
}
// Multi-round resolution.
while (toResolve.length != 0) {
let remaining: { unresolvedDeps: Set<string>, definition: NamedObjectDefinition }[] = [];
let resolvedThisRound = 0;
// One round to resolved each unresolved.
for (let record of toResolve) {
let unresolvedDeps = Object.keys(record.unresolvedDeps);
// For each direct dependency, add their resolved dependency closure to current one.
for (let dep of unresolvedDeps) {
let depClosure = resolved.get(dep);
if (depClosure != null) {
record.unresolvedDeps.delete(dep);
depClosure.forEach(depName => {
record.definition.dependencies.setObjectDependency(depName);
});
}
}
// All unresolved dependencies are already resolved.
if (unresolvedDeps.length == 0) {
resolved.set(record.definition.name, record.definition.dependencies.objectDependencies);
++resolvedThisRound;
}
else {
remaining.push(record);
}
}
if (resolvedThisRound == 0) {
throw new Error("Undefined named object or cyclic dependencies found: '"
+ toResolve.map(obj => { return obj.definition.name; }).join(","))
+ "'.";
}
toResolve = remaining;
}
}
/// <summary> Analyze direct dependencies from a JS value. </summary>
private static analyzeDirectDependencies(dep: ObjectContextDependency, jsValue: any): void {
if (typeof jsValue === 'string') {
let ret = Uri.tryParse(jsValue);
if (ret.success) {
dep.setProtocolDependency(ret.uri.protocol);
}
}
else if (typeof jsValue === 'object') {
let typeName = jsValue['_type'];
if (typeName != null) {
dep.setTypeDependency(typeName);
}
let propertyNames = Object.getOwnPropertyNames(jsValue);
for (let propertyName of propertyNames) {
ScopedObjectContextDefinition.analyzeDirectDependencies(dep, jsValue[propertyName]);
}
}
}
}

6
lib/object-model.ts Normal file
Просмотреть файл

@ -0,0 +1,6 @@
// Facade for exporting object-model interfaces and classes.
export * from './object-type';
export * from './object-provider';
export * from './named-object';
export * from './object-context';

194
lib/object-provider.ts Normal file
Просмотреть файл

@ -0,0 +1,194 @@
import * as path from 'path';
import * as utils from './utils';
import { ObjectContext } from './object-context';
//////////////////////////////////////////////////////////////////////////
/// Interfaces and classes for URI based object retrieval.
/// <summary> Class that encapsulate parsing on URI.
/// A URI in Napa is defined in syntax: <protocol>:/<path>[?<param1>=<value1>[&<param2>=<value2>]*]
/// e.g. doc:/1E2B3C?env=os-prod-co3&type=js
/// TODO: replace this with 'url' module from Node.JS.
/// </summary>
export class Uri {
/// <summary> Protocol of a URI. Case insensitive.
/// e.g. For URI 'doc:/1e2bcd3a?a=1&b=2', protocol is 'doc'.
/// </summary>
public protocol: string = null;
/// <summary> Path of a URI. Case insensitive.
/// e.g. For URI 'doc:/1e2bcd3a?a=1&b=2', path is '1e2bcd3a'.
/// </summary>
public path: string = null;
/// <summary> Parameter map of a URI. Parameters are accessed by getParameter and setParameter methods.
/// e.g, For URI 'doc:/1e2bcd3a?a=1&b=2', parameters are { 'a' : 1, 'b' : 2}
/// </summary>
private _parameters: { [key: string]: string } = {};
/// <summary> Parse string input. Throws exception if parsing failed. </summary>
/// <param name="input"> A URI string </param>
/// <returns> A Uri object </returns>
public static parse(input: string): Uri {
let result = this.tryParse(input);
if (!result.success) {
throw new Error("Invalid URI string '" + input + "'");
}
return result.uri;
}
/// <summary> Try to parse string input. Returns results and uri. </summary>
/// <param name="input"> A URI string </param>
/// <returns> A result structue that contains success indicator and a created Uri object. Uri object will be null if success flag is false. </returns>
public static tryParse(input: string): { success: boolean, uri: Uri } {
// Syntax: <protocol>:/<path>[?<param1>=<value1>[&<param2>=<value2>]*]
let start = 0;
let end = input.indexOf(":/");
if (end <= 0) {
return { success: false, uri: null };
}
let uri = new Uri();
uri.protocol = input.substring(start, end);
// Find path.
let path = "";
start = end + 2;
end = input.indexOf("?", start);
if (end == -1) {
// No parameters.
uri.path = input.substring(start);
}
else {
// Has parameters.
uri.path = input.substring(start, end);
let kvStrings = input.substring(end + 1).split('&');
for (let i = 0; i < kvStrings.length; ++i) {
let cols = kvStrings[i].split('=');
if (cols.length != 2) {
return { success: false, uri: null };
}
uri.setParameter(cols[0], cols[1]);
}
}
return { success: true, uri: uri };
}
/// <summary> Retrieve parameter value by name </summary>
/// <param name="name"> Case insensitive name. </param>
/// <returns> Parameter value as string. </returns>
public getParameter(name: string): string {
return this._parameters[name.toLowerCase()];
}
/// <summary> Set paramter value by name. </summary>
/// <param name="name"> Case-insensitive name. </param>
/// <param name="value"> Parameter value. Suggested interpretion in case-insensitive manner. </param>
public setParameter(name: string, value: string) {
this._parameters[name.toLowerCase()] = value;
}
/// <summary> Tell if an input string is a URI or not. </summary>
/// <param name="input"> Input string. </param>
/// <returns> True if input is a URI, otherwise False. </returns>
public static isUri(input: string): boolean {
return Uri.tryParse(input).success;
}
}
/// <summary> Function interface that takes a Uri as parameter and produce a JS value </summary>
export type ObjectLoader = (uri: Uri | Uri[], context?: ObjectContext) => any;
/// <summary> Interface for provider that retrieve object via a URI </summary>
export interface ObjectProvider {
/// <summary> Provide any JS value from a URI. </summary>
/// <param name="uri"> a URI object or array of URIs. </param>
/// <param name="context"> Object context if needed to create sub-objects. </param>
/// <returns> Any JS value. </returns>
/// <remarks>
/// On implementation, you can check whether input is array or not as Array.isArray(input).
/// </remarks>
provide(uri: Uri | Uri[], context?: ObjectContext): any;
/// <summary> Check if current provider support a protocol name.</summary>
/// <param name="protocol"> Case insensitive protocol name. </param>
/// <returns> True if protocol is supported, otherwise false. </param>
supports(protocol: string): boolean;
}
/// <summary> Object provider definition to register a URI based object provider in Napa. </summary>
export interface ProviderDefinition {
protocol: string;
description?: string;
moduleName: string;
functionName: string;
override?: boolean;
exampleUri?: string[];
}
/// <summary> An implementation of ObjectProvider via protocol based registry. </summary>
export class ProviderRegistry implements ObjectProvider {
/// <summary> Map of protocol (lower-case) to object loader. </summary>
private _protocolToLoaderMap: Map<string, ObjectLoader> = new Map<string, ObjectLoader>();
/// <summary> Provide any JS value from a URI object.
/// <param name="uri"> a URI object or URI array </param>
/// <returns> Any JS value. </returns>
public provide(uri: Uri | Uri[], context?: ObjectContext): any {
let protocol: string;
if (Array.isArray(uri)) {
if ((<Uri[]>uri).length == 0) {
return null;
}
protocol = (<Uri[]>uri)[0].protocol;
for (let item of uri) {
if (item.protocol !== protocol) {
throw new Error("Protocol must the the same with an array of Uris when calling ObjectContext.create.");
}
}
}
else {
protocol = (<Uri>uri).protocol;
}
let lowerCaseProtocol = protocol.toLowerCase();
if (this.supports(lowerCaseProtocol)) {
return this._protocolToLoaderMap.get(lowerCaseProtocol)(uri, context);
}
throw new Error("Unsupported protocol '" + protocol + "'.");
}
/// <summary> Register an object provider with a protocol. Later call to this method on the same protocol will override the provider of former call.</summary>
/// <param name="type"> Case insensitive protocol name.</param>
/// <param name="creator"> An object provider.</param>
public register(protocol: string, loader: ObjectLoader): void {
this._protocolToLoaderMap.set(protocol.toLowerCase(), loader);
}
/// <summary> Check if current provider support a protocol name.</summary>
/// <param name="protocol"> Case insensitive protocol name. </param>
/// <returns> True if protocol is supported, otherwise false. </param>
public supports(protocol: string): boolean {
return this._protocolToLoaderMap.has(protocol.toLowerCase());
}
/// <summary> Created ProviderRegistry from a collection of ProviderDefinition objects. </summary>
/// <param name="providerDefCollection"> Collection of ProviderDefinition objects. </summary>
/// <param name="baseDir"> Base directory name according to which module name will be resolved.</param>
/// <returns> A ProviderRegistry object. </returns>
public static fromDefinition(providerDefCollection: ProviderDefinition[], baseDir: string): ProviderRegistry {
let registry = new ProviderRegistry();
if (providerDefCollection != null) {
for (let def of providerDefCollection) {
let moduleName = def.moduleName;
if (!path.isAbsolute(def.moduleName)) {
moduleName = path.resolve(baseDir, moduleName);
}
let creator = utils.loadFunction(moduleName, def.functionName);
registry.register(def.protocol, creator);
}
}
return registry;
}
}

121
lib/object-type.ts Normal file
Просмотреть файл

@ -0,0 +1,121 @@
import * as path from 'path';
import * as utils from './utils';
import { ObjectContext } from './object-context';
//////////////////////////////////////////////////////////////////////////////////////
// Interfaces and classes for object creation.
/// <summary> Interface for objects with '_type' property, which is used to determine object creator.
/// '_type' property is case-sensitive.
/// </summary>
export interface ObjectWithType {
_type: string
};
/// <summary> Object type definition to register a type in Napa. </summary>
export interface TypeDefinition {
typeName: string;
description?: string;
moduleName: string;
functionName: string;
override?: boolean;
exampleObjects?: any[];
}
/// <summary> Function interface for object constructor, which takes an input object and produce an output object. </summary>
export interface ObjectConstructor {
(input: ObjectWithType | ObjectWithType[], context?: ObjectContext): any
}
/// <summary> Interface for object factory. </summary>
export interface ObjectFactory {
/// <summary> Create an output JS value from input object. </summary>
/// <param name="input"> Object with '_type' property or object array. </param>
/// <param name="context"> Context if needed to construct sub-objects. </param>
/// <returns> Any JS value type. </returns>
/// <remarks>
/// When input is array, all items in array must be the same type.
/// On implementation, you can check whether input is array or not as Array.isArray(input).
/// Please refer to example\example_types.ts.
/// </remarks>
create(input: ObjectWithType | ObjectWithType[], context?: ObjectContext): any;
/// <summary> Check whether current object factory support given type. </summary>
/// <param name="typeName"> value of '_type' property. </param>
/// <returns> True if supported, else false. </returns>
supports(typeName: string): boolean;
}
/// <summary> An implementation of ObjectFactory that allows to register type with their creator. </summary>
export class TypeRegistry implements ObjectFactory {
/// <summary> Type name to creator map. </summary>
private _typeToCreatorMap: Map<string, ObjectConstructor> = new Map<string, ObjectConstructor>();
/// <summary> Create an output object from input object. Exception will be thrown if type is not found in current application. </summary>
/// <param name="input"> Input object with a property '_type', the value of '_type' should be registered in current application. </param>
/// <returns> Object created from input. </returns>
/// <remarks> When input is array, all items in array must be the same type.</remarks>
public create(input: ObjectWithType | ObjectWithType[], context?: ObjectContext): any {
if (input == null) {
return null;
}
let typeName: string;
if (Array.isArray(input)) {
if ((<ObjectWithType[]>input).length == 0) {
return [];
}
// It assumes that all items in array are the same type.
typeName = (<ObjectWithType[]>input)[0]._type;
for (let elem of input) {
if (elem._type !== typeName) {
throw new Error("Property '_type' must be the same for all elements in input array when calling ObjectFactory.create.");
}
}
}
else {
typeName = (<ObjectWithType>input)._type;
}
if (this.supports(typeName)) {
return this._typeToCreatorMap.get(typeName)(input, context);
}
throw new Error("Not supported type: '" + typeName + "'.");
}
/// <summary> Register an object creator for a type. Later call to this method on the same type will override the creator of former call.</summary>
/// <param name="type"> Case sensitive type name.</param>
/// <param name="creator"> Function that takes one object as input and returns an object.</param>
public register(type: string, creator: ObjectConstructor): void {
this._typeToCreatorMap.set(type, creator);
}
/// <summary> Check if current type registry contain a type.</summary>
/// <param name="type"> Case sensitive type name. </param>
/// <returns> True if type is registered, otherwise false. </param>
public supports(typeName: string): boolean {
return this._typeToCreatorMap.has(typeName);
}
/// <summary> Create TypeRegistry from a collection of TypeDefinitions </summary>
/// <param name="typeDefCollection"> A collection of type definitions </param>
/// <param name="baseDir"> Base directory name according to which module name will be resolved. </param>
/// <returns> A TypeRegistry object. </returns>
public static fromDefinition(typeDefCollection: TypeDefinition[], baseDir: string): TypeRegistry {
let registry = new TypeRegistry();
if (typeDefCollection != null) {
typeDefCollection.forEach(def => {
let moduleName = def.moduleName;
if (def.moduleName.startsWith(".")) {
moduleName = path.resolve(baseDir, moduleName);
}
let creator = utils.loadFunction(moduleName, def.functionName);
registry.register(def.typeName, creator);
});
}
return registry;
}
}

20
lib/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"noImplicitAny": true,
"removeComments": false,
"preserveConstEnums": true,
"baseUrl": ".",
"declaration": true,
"lib": [
"es2015"
],
"outDir": "objd/amd64/node_modules/winery/lib",
"declarationDir": "objd/amd64/node_modules/winery/types"
},
"exclude": [
"objd",
"obj",
"**/.vscode"
]
}

681
lib/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,681 @@
//////////////////////////////////////////////////////////////////
// This file defines utility classes used in Napa
//
import fs = require('fs');
// TODO: replace bundle with node module dependencies once 'url' and 'events' are introduced in Napa.JS.
var Ajv = require('./ajv-bundle');
var xml2js = require('./xml2js-bundle');
/// TODO: move config reading to @napajs/config module.
/// <summary> Class that wraps JSON schema validation. </summary>
export class JsonSchema {
private _fileName: string;
private _validator: any;
/// <summary> Static AJV engine to create pre-compiled validator </summary>
/// This path is to be compatible with V8 isolates/
private static _ajv = Ajv();
/// <summary> Validate a JS value against current schema. </summary>
/// <param name="jsValue"> a JS value </param>
/// <returns> True if validation succeeded, otherwise false. </returns>
public validate(jsValue: any): boolean {
return this._validator(jsValue);
}
/// <summary> Get schema file name. </summary>
/// <returns> Schema file name. </returns>
public getFileName(): string {
return this._fileName;
}
/// <summary> Get errors from previous validation. </summary>
/// <returns> Errors from previous validation. </returns>
public getErrors(): string {
return this._validator.errors;
}
/// <summary> Constructor </summary>
/// <param name="fileName"> JSON schema file name.</param>
public constructor(fileName: string) {
this._fileName = fileName;
var schemaObject = parseJsonFile(fileName);
/// This path is to be compatible with V8 isolates/
this._validator = JsonSchema._ajv.compile(schemaObject);
}
}
/// <summary> Ensure a JS value match a JSON schema. Throws exception if it doesn't match. </summary>
/// <param name="jsValue"> Any JS value type. </param>
/// <param name="jsonSchema"> JSON schema object. </param>
export function ensureSchema(jsValue: any, jsonSchema: JsonSchema): void {
if (!jsonSchema.validate(jsValue)) {
throw new Error(
"Object '"
// + JSON.stringify(jsValue)
+ "' doesn't match schema '"
+ jsonSchema.getFileName() + "':"
+ JSON.stringify(jsonSchema.getErrors()));
}
}
/// <summary> Parse JSON string into JS value. Validate with json schema if present. </summary>
/// <param name="jsonString"> JSON string. </param>
/// <param name="jsonSchema"> JSON schema object. Optional </param>
/// <param name="allowComments"> Whether allow comments in JSON.
/// REMARKS: PLEASE NOT TO ENABLE THIS DURING QUERY PROCESSING, WHICH IS REALLY SLOW < /param>
/// <returns> JS value. Throws exception if parse failed or schema validation failed. </returns>
export function parseJsonString(jsonString: string,
jsonSchema: JsonSchema = undefined,
allowComments: boolean = false): any {
/// This path is to be compatible with V8 isolates/
if (allowComments) {
var stripJsonComments = require('strip-json-comments');
jsonString = stripJsonComments(jsonString);
}
try {
var jsValue = JSON.parse(jsonString);
}
catch (error) {
throw new Error("Failed to parse JSON ':" + error.toString());
}
if (jsonSchema != null) {
ensureSchema(jsValue, jsonSchema);
}
return jsValue;
}
/// <summary> Parse JSON file.
/// Throw exception if parse failed or JSON schema validation failed.
/// </summary>
/// <param name="jsonFile"> JSON file to parse. </param>
/// <param name="jsonSchema"> JSON schema object. </param>
/// <param name="allowComments"> Whether allow comments in JSON. </param>
/// <returns> JS value parsed from JSON file. </returns>
export function parseJsonFile(jsonFile: string,
jsonSchema: JsonSchema = undefined,
allowComments: boolean = false): any {
return appendMessageOnException(
".Error file name: '" + jsonFile + "'.",
() => { return parseJsonString(readJsonString(jsonFile), jsonSchema, allowComments); });
}
/// <summary> Read JSON string from file. </summary>
export function readJsonString(jsonFile: string): string {
return fs.readFileSync(jsonFile, 'utf8').replace(/^\uFEFF/, '');
}
/// <summary> Parse JS values from an XML file. Please see summary of XmlObjectReader class for details. </summary>
/// <param name="xmlFilePath"> XML file path. </param>
/// <param name="jsonSchema"> JSON schema to validate object. optional. </param>
/// <returns> JS values if parsed successfully. Throw exception if failed. </returns>
export function parseXmlFile(xmlFilePath: string, jsonSchema?: JsonSchema): any {
return XmlObjectReader.default.readFile(xmlFilePath, jsonSchema);
}
/// <summary> Parse JS values from an XML string. Please see summary of XmlObjectReader class for details. </summary>
/// <param name="xmlString"> XML string. </param>
/// <param name="jsonSchema"> JSON schema to validate object. optional. </param>
/// <returns> JS values if parsed successfully. Throw exception if failed. </returns>
export function parseXmlString(xmlString: string, jsonSchema?: JsonSchema): any {
return XmlObjectReader.default.read(xmlString, jsonSchema);
}
/// <summary> Read JS value from a config file with extension '.config' OR '.json' in XML format.
/// When file path ends with '.config', it will first try to read object from '.config',
/// if file '.config' is not presented, '.json' will be checked.
/// When file path ends with '.json', only '.json' will be checked.
///
/// This method is introduced to make Napa code transparent to the format of configuration file,
/// We can use Autopilot flattened XML file (with enables bed-specific configuration) or plain JSON file.
/// </summary>
/// <param name='filePath'> File path with ".config" or ".json" extension </param>
/// <returns> JS values if parsed successfully. Throw exception if failed. </returns>
export function readConfig(filePath: string, jsonSchema?: JsonSchema): any {
var extensionStart = filePath.lastIndexOf('.');
var extension = filePath.substring(extensionStart).toLowerCase();
var checkForConfigExtFirst = false;
if (extension == ".config") {
checkForConfigExtFirst = true;
}
else if (extension != '.json') {
throw new Error("readConfig only support '.config' and '.json' as extension. filePath='"
+ filePath + "'.");
}
if (checkForConfigExtFirst) {
// Then check XML format .config file.
var xmlConfigFile = filePath;
if (fs.existsSync(xmlConfigFile)) {
// TODO: dump converted JSON for debug purpose.
return parseXmlFile(xmlConfigFile, jsonSchema);
}
}
// Check JSON format .json file if XML format is not present.
var jsonFile = filePath.substring(0, extensionStart) + ".json"
// We allow comments in JSON configuration files.
return parseJsonFile(jsonFile, jsonSchema, true);
}
/// <summary> Interface for JS value transformation. </summary>
export interface Transform {
apply(jsValue: any): any;
};
export class ChainableTransform implements Transform {
protected _next: ChainableTransform = null;
public add(next: ChainableTransform): ChainableTransform {
var node: ChainableTransform = this;
while (node._next != null) {
node = node._next;
}
node._next = next;
return this;
}
public apply(jsValue: any): any {
var transformedValue = this.transform (jsValue);
if (this._next != null) {
return this._next.apply(transformedValue);
}
return transformedValue;
}
protected transform(jsValue: any): any {
throw new Error("Not implemented");
}
};
/// <summary> Rename properties of a JS object </summary>
export class RenameProperties extends ChainableTransform {
private _nameMap: { [oldName: string]: string };
/// <summary> Constructor </summary>
/// <param name="nameMap"> Old name to new name mapping. </param>
public constructor(nameMap: { [oldName: string]: string }) {
super();
this._nameMap = nameMap;
}
/// <summary> Do transformation by rename properties. </summary>
/// <param name="jsObject"> container JS object to rename properties. </param>
public transform(jsObject: {[propertyName:string]: any}): any {
var oldNames: string[] = Object.keys(this._nameMap);
oldNames.forEach(oldName => {
jsObject[this._nameMap[oldName]] = jsObject[oldName];
delete jsObject[oldName];
});
return jsObject;
}
}
/// <summary> Set default value for properties that are undefined or null </summary>
export class SetDefaultValue extends ChainableTransform {
private _defaultValueMap: { [propertyName: string]: any };
/// <summary> Constructor </summary>
/// <param name="nameMap"> Property name to default value map. </param>
public constructor(defaultValueMap: { [propertyName: string]: any }) {
super();
this._defaultValueMap = defaultValueMap;
}
/// <summary> Do transformation by set default values for fields that does't appear in object. </summary>
/// <param name="jsObject"> JS object </param>
public transform(jsObject: {[propertyName:string]: any}): any {
var propertyNames: string[] = Object.keys(this._defaultValueMap);
propertyNames.forEach(propertyName => {
if (!jsObject.hasOwnProperty(propertyName)) {
jsObject[propertyName] = this._defaultValueMap[propertyName];
}
});
return jsObject;
}
}
/// <summary> Interface for value transform function </summary>
export interface ValueTransform {
(input: any): any
}
/// <summary> Value transfor of a JS value to another </summary>
export class TransformPropertyValues extends ChainableTransform {
private _propertyNameToTransformMap: { [propertyName: string]: ValueTransform };
/// <summary> Constructor </summary>
/// <param name="nameMap"> Property name to value transform function. </param>
public constructor(propertyNameToTransformMap: { [propertyName: string]: ValueTransform }) {
super();
this._propertyNameToTransformMap = propertyNameToTransformMap;
}
/// <summary> Do transformation by transforming values on properties. </summary>
/// <param name="jsObject"> container JS object. </param>
public transform(jsObject: { [propertyName: string]: any }): any {
var oldNames: string[] = Object.keys(this._propertyNameToTransformMap);
oldNames.forEach((propertyName: string) => {
jsObject[propertyName] = this._propertyNameToTransformMap[propertyName](jsObject[propertyName]);
});
return jsObject;
}
}
/// <summary> An operation on a singular node that produce a output
/// A singular node is a JS node which is a object like { } or an 1-element array. [{ //...}]
/// </summary>
interface SingularNodeOperation {
(node: any): any;
}
/// <summary> Result from a singular node operation. </summary>
/// operationPerformed indicates if a target is a singular node or not.
/// result is the returned value from the operation.
class SingularNodeOperationResult {
public constructor(public operationPerformed: boolean, public result?: any) {
}
}
/// <summary> A JS Object reader from XML file or string </summary>
/// We don't support arbitrary XML compositions from the input. Instead, we carefully
/// supported a sub-set of XML composition patterns that can translate to any JS object.
///
/// 3 types of XML composition patterns are supported that map to 3 types of JS objects.
///
/// 1. Property: Used for translating to a JS property.
/// <property1>value</property1>
///
/// Converted JS form would be: "property1": "value".
/// If the XML element is the root element, converted JS would simply be: "value".
///
/// Empty property is not valid semantically.
/// <property1/> will be translated as an empty object as 'property1': {}.
///
/// 2. List: Used for translating to a JS list.
/// "item" is used as the default XML element name for list items, which can contain either raw content or nested nodes.
/// <list1>
/// <item>value1</item>
/// <item>value2</item>
/// </list1>
///
/// We can use different XML element name by specifying 'itemElement' attributes at the list element.
/// <list1 itemElement='customizedItem'>
/// <customizedItem>value1</customizedItem>
/// <customizedItem>value2</customizedItem>
/// </list1>
///
/// Both examples above are converted to JS form like: "list1": ["value1", "value2"]
/// If list is root element in the XML, the output will be ["value1", "value2"].
///
/// Empty list will be represented as <list1 type='list'/>, which will be translated to "list1": [].
/// Without specifying 'type' attribute, <list1/> will be translated to "list1": {}.
///
/// 3. Object: Used for translating to a JS object.
/// Object node can have nested properties and lists.
/// <object1>
/// <property1>value</property1>
/// <list1>
/// <item>value1</item>
/// <item>true</item>
/// <item>2</item>
/// <item type="string">false</item>
/// <item type="string">3</item>
/// </list1>
/// </object1>
///
/// Converted JS form would be: "object1" : { "property1": "value", "list1": ["value1", true, 2, "false", "3"] }
/// If 'object1' is the root element, the JS form would be { "property1": "value", "list1": ["value1", true, 2, "false", "3"]}
///
/// Empty object will be reprsented as <object1/>, which will be translated to "object1": {}.
///
/// No attribute could be used in all XML element with 3 exceptions:
/// 1. attribute 'type'='string' | 'bool' | 'number' can be used to explicitly specify value type despite its value content.
/// 2. attribute 'type'="list' can be used to sepcify empty XML element to hint a empty list.
/// 3. attribute 'itemElement' can be used to speicfy customized XML element name for list items other than 'item'.
class XmlObjectReader {
/// <summary> xml2js parser. </summary>
private _parser: any;
/// <summary> XML builder when we want to restore xml2js output to XML for error reporting. </summary>
private _builder: any;
/// <sumary> Default instance. </summary>
private static _defaultInstance: XmlObjectReader = new XmlObjectReader();
/// <summary> Return a default instance of XmlObjectReader </summary>
public static get default(): XmlObjectReader {
return this._defaultInstance;
}
public constructor() {
this._parser = new xml2js.Parser({
explicitRoot: false,
explicitCharkey: true,
preserveChildrenOrder: true,
});
this._builder = new xml2js.Builder({
headless: true,
});
}
/// <summary> Read a JS object from a XML file. </summary>
/// <param name="xmlFile"> an XML file path. </param>
/// <param name="jsonSchema"> JSON schema to validate object. optional. </param>
/// <returns> JS object. </returns>
public readFile(xmlFile: string, jsonSchema?: JsonSchema): any {
var xmlString = fs.readFileSync(xmlFile, 'utf8');
return this.read(xmlString, jsonSchema);
}
/// <summary> Read a JS object from a XML string. </summary>
/// <param name="xmlString"> An XML string. </param>
/// <param name="jsonSchema"> JSON schema to validate object. optional. </param>
/// <returns> JS object. </returns>
public read(xmlString: string, jsonSchema?: JsonSchema): any {
var output: any = null;
this._parser.parseString(xmlString, (error: any, result:any) => {
output = this._transformNode(result);
});
if (jsonSchema) {
ensureSchema(output, jsonSchema);
}
return output;
}
/// <summary> Transform a JS node after xml2js </summary>
/// <param name="node"> Input node </param>
/// <returns> Output JS node. </returns>
private _transformNode(node: any): any {
if (this._isProperty(node)) {
return this._transformProperty(node);
}
else if (this._isList(node)) {
return this._transformList(node);
}
else if (this._isObject(node)) {
return this._transformObject(node);
}
else {
throw new Error("Unsupported JSON pattern converted from XML composition:"
+ this._builder.buildObject(node));
}
}
/// <summary> Check if a node is transformed from XML element has a specific attribute</summary>
/// <param name="node"> Input node </param>
/// <param name="attributeName"> Attribute name </param>
/// <returns> True if has this attribute, otherwise false. </returns>
private _hasXmlAttribute(node: any, attributeName: string): boolean {
var ret = this._tryOperateOnSingularNode(node, (input) => {
return input.hasOwnProperty("$") && input["$"].hasOwnProperty(attributeName);
});
return ret.operationPerformed && ret.result;
}
/// <summary> Get the value of XML attribute name from a node. </summary>
/// <param name="node"> Input node </param>
/// <param name="attributeName"> Attribute name </param>
/// <returns> The value of the XML attribute. Throws exception if no such XML attribute. </returns>
private _getXmlAttribute(node: any, attributeName: string): any {
if (!this._hasXmlAttribute(node, attributeName)) {
throw new Error("XML attribute '" + attributeName + "' doesn't exist in object: " + JSON.stringify(node));
}
return this._operateOnSingularNode(node, (input) => {
return input["$"][attributeName];
});
}
/// <summary> Perform operation on a singular node (a object or an 1-element array). </summary>
/// <param name="node"> Input node </param>
/// <param name="operation"> Operation </param>
/// <returns> Return value from the operation. Exception will be thrown if the node is not a singular node. </returns>
private _operateOnSingularNode(node: any, operation: SingularNodeOperation): any {
var input = node;
if (Array.isArray(node)) {
if (node.length != 1) {
throw new Error("Cannot perform singular node operation on an array which has more than one element."
+ JSON.stringify(node));
}
input = node[0];
}
return operation(input);
}
/// <summary> Try perform operation on a singular node (a object or an 1-element array). </summary>
/// <param name="node"> Input node </param>
/// <param name="operation"> Operation </param>
/// <returns> Return SingularNodeOperationResult. No exception will be thrown if node is not a singular node. </returns>
private _tryOperateOnSingularNode(node: any, operation: SingularNodeOperation): SingularNodeOperationResult {
var input = node;
if (Array.isArray(node)) {
if (node.length != 1) {
return new SingularNodeOperationResult(false);
}
input = node[0];
}
return new SingularNodeOperationResult(true,
operation(input));
}
// Property XML element is mapped into JS object by xml2js in 2 ways.
// 1. If the element is the only element with the name under its container, like
// <container>
// <name>value</name>
// </container>
// the <name> element will be transformed to a property of its container:
// "name" : [{ "_": "value" }]
//
// 2. If there are multiple elements using the same name under its container, like:
// <container>
// <name>value1 </name>
// <name>value2 </name >
// </container>
// It will be transformed to a property of its container like:
// "name": [
// { "_": "value1" },
// { "_": "value2" },
// ]
//
// Here we check if a jsNode is a leaf XML element.
private _isProperty(node: any): boolean {
var ret = this._tryOperateOnSingularNode(node, (singleNode) => {
// Empty object container.
if (singleNode === "") {
return false;
}
// Has content like <name>value</name>
return singleNode.hasOwnProperty("_");
});
return ret.operationPerformed && ret.result;
}
// For property, by default string will be the value type. unless specified with 'type' property.
// <elem>true</elem> will be transform to: "elem": true
// <elem type="string">true</elem> will be transform to: "elem": "true".
// <elem>1</elem> will be transform to: "elem": 1.
// <elem type="string">1</elem> will be transform to: "elem": "1".
private _transformProperty(node: any): any {
// NOTE: till here _isLeaf already performed on node.
return this._operateOnSingularNode(node, (singleNode) => {
var value: string = singleNode["_"];
// If 'type' property is specified.
var valueType: string = undefined;
if (this._hasXmlAttribute(singleNode, 'type')) {
valueType = this._getXmlAttribute(singleNode, 'type').toLowerCase();
}
// Short-circuit explict string type.
if (valueType === 'string') {
return value;
}
// Auto detect bool.
if (value === 'true' || value === 'false') {
return value === 'true';
}
else if (valueType === 'bool') {
throw new Error("Invalid bool value: " + value);
}
// Auto detect number.
var numValue = Number(value);
if (!isNaN(numValue)) {
return numValue;
}
else if (valueType === 'number') {
throw new Error("Invalid number value: " + value);
}
return value;
});
}
// List node is mapped from XML to JS in a way of this pattern:
// <list>
// <!-- item is a leaf type -->
// <item>value1</item>
// <!-- item is a container type -->
// <item><property2>value2</property2></item>
// </list>
// It will be transformed to JS object like:
// "list" : [
// { "item" [
// { "_": "value1"},
// { "property2" : [{ "_": value2 }] },
// ]}]
//
// Empty list <list type='list'/> will be transformed to
// "list" : [{"$": { "type": "list" }}]
private _isList(node: any): boolean {
var ret = this._tryOperateOnSingularNode(node, (input) => {
// Check for non-empty list.
var props = Object.getOwnPropertyNames(input);
var itemElementName = 'item';
var expectedProps = 1;
if (this._hasXmlAttribute(input, 'itemElement')) {
itemElementName = this._getXmlAttribute(input, 'itemElement');
++expectedProps;
}
if (input.hasOwnProperty(itemElementName) && props.length == expectedProps) {
return true;
}
// Check for empty list.
if (input.hasOwnProperty('$') && props.length == 1) {
var nodeType = input['$']['type'];
if (nodeType != 'list') {
throw new Error("Only 'type' attribute is supported for empty XML element for denoting it is a list.");
}
return true;
}
return false;
});
return ret.operationPerformed && ret.result;
}
private _transformList(node: any): any[] {
return this._operateOnSingularNode(node, (input) => {
// Empty list.
if (this._hasXmlAttribute(input, 'type')) {
return [];
}
// List with customize item element.
var itemElementName = 'item'
if (this._hasXmlAttribute(input, 'itemElement')) {
itemElementName = this._getXmlAttribute(input, 'itemElement');
}
var list: any[] = []
input[itemElementName].forEach((item: any) => {
list.push(this._transformNode(item));
});
return list;
});
}
private _isObject(node: any): boolean {
var ret = this._tryOperateOnSingularNode(node, (input) => {
// Object cannot have content like leaf node.
if (input.hasOwnProperty('_')) {
return false;
}
var props = Object.getOwnPropertyNames(input);
if (input.hasOwnProperty('$') && props.length > 1) {
throw new Error("Napa doesn't support XML element with both attributes and sub-elements, except for attribute 'itemElement' for list. Element with issues:\n"
+ this._builder.buildObject(node));
}
return true;
});
return ret.operationPerformed && ret.result;
}
private _transformObject(node: any): any {
return this._operateOnSingularNode(node, (input) => {
if (node == "") {
/// Empty object.
return {};
}
var output: any = {};
var props = Object.getOwnPropertyNames(input);
props.forEach((prop) => {
output[prop] = this._transformNode(input[prop]);
});
return output;
});
}
}
/// <summary> Include file name in exception when thrown.
export function appendMessageOnException(message: string, fun: Function) : any {
try {
return fun();
}
catch (error) {
error.message += message;
throw error;
}
}
/// <summary> Make a return value as a resolved Promise or return if it is already a Promise. </summary>
export function makePromiseIfNotAlready(returnValue: any): Promise<any> {
if (returnValue != null
&& typeof returnValue === 'object'
&& typeof returnValue['then'] === 'function') {
return returnValue;
}
return Promise.resolve(returnValue);
}
/// <summary> Load a function object given a name from a module. </summary>
export function loadFunction(moduleName: string, functionName: string) {
let module = require(moduleName);
if (module == null) {
throw new Error("Cannot load module '" + moduleName + "'.");
}
let func = module;
for (let token of functionName.split(".")) {
if (token.length == 0) {
continue;
}
func = func[token];
if (func == null) {
throw new Error("Cannot load function '"
+ functionName
+ "' in module '"
+ moduleName
+ "'. Symbol '"
+ token
+ "' doesn't exist.");
}
}
return func;
}

168
lib/wire.ts Normal file
Просмотреть файл

@ -0,0 +1,168 @@
import * as objectModel from './object-model';
import * as utils from './utils';
import * as path from 'path';
/////////////////////////////////////////////////////////////////////////////////////////////////
// Interfaces for winery wire format.
/// <summary> Interface for control flags. </summary>
export type ControlFlags = {
/// <summary> Enable debugging or not. </summary>
debug?: boolean;
/// <summary> Return performance numbers or not. </summary>
perf?: boolean;
}
/// <summary> Interface for winery request. </summary>
export interface Request {
/// <summary> Application name </summary>
application: string;
/// <summary> Entry point name </summary>
entryPoint: string;
/// <summary> Trace ID </summary>
traceId?: string;
/// <summary> Input JS object for entry point </summary>
input?: any;
/// <summary> Control flags </summary>
controlFlags?: ControlFlags;
/// <summary> Overridden types </summary>
overrideTypes?: objectModel.TypeDefinition[];
/// <summary> Overridden named objects </summary>
overrideObjects?: objectModel.NamedObjectDefinition[];
/// <summary> Overridden providers </summary>
overrideProviders?: objectModel.ProviderDefinition[];
}
/// <summary> Response code </summary>
export enum ResponseCode {
// Success.
Success = 0,
// Internal error.
InternalError = 1,
// Server side timeout.
ProcessTimeout = 2,
// Throttled due to policy.
Throttled = 3,
// Error caused by bad input.
InputError = 4
}
/// <summary> Exception information in response. </summary>
export type ExceptionInfo = {
stack: string;
message: string;
fileName?: string;
lineNumber?: number;
columnNumber?: number;
}
/// <summary> Debug event in DebugInfo. </summary>
export type DebugEvent = {
eventTime: Date;
logLevel: string;
message: string;
}
/// <summary> Debug information when debug flag is on. </summary>
export type DebugInfo = {
exception: ExceptionInfo;
events: DebugEvent[];
details: { [key: string]: any };
machineName: string;
}
/// <summary> Write performance numbers when perf flag is on. </summary>
export type PerfInfo = {
processingLatencyInMS: number;
}
/// <summary> Interface for response </summary>
export interface Response {
/// <summary> Response code </summary>
responseCode: ResponseCode;
/// <summary> Error message if response code is not Success. </summary>
errorMessage?: string;
/// <summary> Output from entrypoint. </summary>
output?: any;
/// <summary> Debug information. </summary>
debugInfo?: DebugInfo;
/// <summary> Performance numbers. </summary>
perfInfo?: PerfInfo;
}
/// <summary> Request helper. </summary>
export class RequestHelper {
/// <summary> JSON schema for resquest. </summary>
private static readonly REQUEST_SCHEMA: utils.JsonSchema = new utils.JsonSchema(
path.resolve(path.resolve(__dirname, '../schema'), "request.schema.json"));
/// <summary> Set default values transform. </summary>
private static _transform = new utils.SetDefaultValue({
traceId: "Unknown",
overrideObjects: [],
overrideProviders: [],
overrideTypes: [],
controlFlags: {
debug: false,
perf: false
}
});
/// <summary> Tell if a jsValue is a valid request at run time. </summary>
public static validate(jsValue: any): boolean {
return this.REQUEST_SCHEMA.validate(jsValue);
}
/// <summary> Create request from a JS value that conform with request schema. </summary>
public static fromJsValue(jsValue: any): Request {
if (!this.validate(jsValue))
throw new Error("Request doesn't match request schema.");
let request = <Request>(jsValue);
this._transform.apply(request);
// TODO: @dapeng, make SetDefaultValue recursive.
if (request.controlFlags.debug == null) {
request.controlFlags.debug = false;
}
if (request.controlFlags.perf == null) {
request.controlFlags.perf = false;
}
return request;
}
}
/// <summary> Response helper. </summary>
export class ResponseHelper {
/// <summary> JSON schema for response. </summary>
private static readonly RESPONSE_SCHEMA: utils.JsonSchema = new utils.JsonSchema(
path.resolve(path.resolve(__dirname, '../schema'), "response.schema.json"));
/// <summary> Parse a JSON string that conform with response schema. </summary>
public static parse(jsonString: string): Response {
let response = utils.parseJsonString(jsonString, this.RESPONSE_SCHEMA);
return <Response>(response);
}
/// <summary> Validate a JS value against response schema. </summary>
public static validate(jsValue: any): boolean {
return this.RESPONSE_SCHEMA.validate(jsValue);
}
}

10565
lib/xml2js-bundle.js Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

18
package.json Normal file
Просмотреть файл

@ -0,0 +1,18 @@
{
"name": "winery",
"version": "0.0.1",
"author": "napajs",
"description": "Framework for building highly iterative applications in JavaScript.",
"keywords": ["winery", "application framework", "dependency injection", "iterative", "continuous modification"],
"main": "./lib/index.js",
"types": "./types/index.d.ts",
"readme": "./README.md",
"dependencies": {
"strip-json-comments": ">= 2.0.0"
},
"devDependencies": {
"@types/node": ">= 7.0.8",
"@types/mocha": ">= 2.2.0",
"@types/xml2js": ">= 0.0.1"
}
}

Просмотреть файл

@ -0,0 +1,121 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/application-config",
"type": "object",
"title": "Vineyard application configuration schema.",
"description": "This JSON schema defines the root JSON file to define a application.",
"name": "/",
"properties": {
"id": {
"id": "http://winery.napajs.org/application-config/id",
"type": "string",
"title": "Application ID.",
"description": "Required. Application id which is unique in Napa.",
"name": "id"
},
"description": {
"id": "http://winery.napajs.org/application-config/description",
"type": "string",
"title": "Application description.",
"description": "Required. Purpose and scope of this application.",
"name": "description"
},
"allowPerRequestOverride": {
"id": "http://winery.napajs.org/object-type-config/item/allowPerRequestOverride",
"type": "boolean",
"title": "Allow per-request override schema.",
"description": "Whether to allow per-request override for this application.",
"name": "allowPerRequestOverride",
"default": true
},
"defaultExecutionStack": {
"id": "http://winery.napajs.org/application-config/defaultExecutionStack",
"type": "array",
"title": "Default execution stack consists of a list of interceptor names.",
"description": "Optional. If not specified, will inherit from Engine configuration.",
"name": "defaultExecutionStack",
"items": {
"id": "http://winery.napajs.org/application-config/defaultExecutionStack/0",
"type": "string",
"title": "Interceptor name",
"description": "Interceptor are stacked as execution stack. ."
}
},
"objectTypes": {
"id": "http://winery.napajs.org/application-config/objectTypes",
"type": "array",
"title": "Object type definition files.",
"description": "Required. Object types supported in this application.",
"name": "objectTypes",
"items": {
"id": "http://winery.napajs.org/application-config/objectTypes/0",
"type": "string",
"title": "Path of an object type definition file",
"description": "Multiple object type definition file is supported."
}
},
"namedObjects": {
"id": "http://winery.napajs.org/application-config/namedObjects",
"type": "array",
"title": "Named objects definition files.",
"description": "Required. Named objects are objects that you want to access them by app.getObject('<name>').",
"name": "namedObjects",
"items": {
"id": "http://winery.napajs.org/application-config/namedObjects/0",
"type": "string",
"title": "Path of a named object definition file.",
"description": "Multiple named object definition file is supported."
}
},
"objectProviders": {
"id": "http://winery.napajs.org/application-config/objectProviders",
"type": "array",
"title": "Object providers definition files.",
"description": "Optional. Object providers are functions that provide objects by URI.",
"name": "objectProviders",
"items": {
"id": "http://winery.napajs.org/application-config/objectProviders/0",
"type": "string",
"title": "Path of an object provider definition file.",
"description": "Multiple object provider definition file is supported."
}
},
"metrics": {
"id": "http://winery.napajs.org/application-config/metrics",
"type": "object",
"title": "Metrics collection definition.",
"description": "Performance metricss definition for current application.",
"name": "metricss",
"additionalProperties": false,
"properties": {
"sectionName": {
"id": "http://winery.napajs.org/application-config/metrics/sectionName",
"type": "string",
"title": "Section name.",
"description": "Section name of performance metrics.",
"name": "sectionName"
},
"definition": {
"id": "http://winery.napajs.org/application-config/metricss/definition",
"type": "array",
"title": "Definition files",
"description": "A list of metrics definition files.",
"name": "definition",
"items": {
"id": "http://winery.napajs.org/application-config/metricss/definition/0",
"type": "string",
"title": "Path of a metrics definition file",
"description": "Multiple metrics definition file is supported."
}
}
},
"required": [ "sectionName", "definition"]
}
},
"additionalProperties": false,
"required": [
"id",
"description",
"namedObjects"
]
}

Просмотреть файл

@ -0,0 +1,86 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/engine-config",
"type": "object",
"title": "Vineyard application configuration schema.",
"description": "This JSON schema defines the root JSON file to define a application.",
"name": "/",
"properties": {
"allowPerRequestOverride": {
"id": "http://winery.napajs.org/object-type-config/item/allowPerRequestOverride",
"type": "boolean",
"title": "Allow per-request override schema.",
"description": "Whether to allow per-request override for this application.",
"name": "allowPerRequestOverride",
"default": true
},
"throwExceptionOnError": {
"id": "http://winery.napajs.org/object-type-config/item/throwExceptionOnError",
"type": "boolean",
"title": "Throw exception on error schema.",
"description": "Whether to throw exception on error, or return error response code.",
"name": "throwExceptionOnError",
"default": true
},
"defaultExecutionStack": {
"id": "http://winery.napajs.org/engine-config/defaultExecutionStack",
"type": "array",
"title": "Default execution stack consists of a list of interceptor names.",
"description": "Optional. If not specified, will inherit from Engine configuration.",
"name": "defaultExecutionStack",
"items": {
"id": "http://winery.napajs.org/engine-config/defaultExecutionStack/0",
"type": "string",
"title": "Interceptor name",
"description": "Interceptor are stacked as execution stack. ."
}
},
"objectTypes": {
"id": "http://winery.napajs.org/engine-config/objectTypes",
"type": "array",
"title": "Object type definition files.",
"description": "Required. Object types supported in this application.",
"name": "objectTypes",
"items": {
"id": "http://winery.napajs.org/engine-config/objectTypes/0",
"type": "string",
"title": "Path of an object type definition file",
"description": "Multiple object type definition file is supported."
}
},
"namedObjects": {
"id": "http://winery.napajs.org/engine-config/namedObjects",
"type": "array",
"title": "Named objects definition files.",
"description": "Required. Named objects are objects that you want to access them by app.getObject('<name>').",
"name": "namedObjects",
"items": {
"id": "http://winery.napajs.org/engine-config/namedObjects/0",
"type": "string",
"title": "Path of a named object definition file.",
"description": "Multiple named object definition file is supported."
}
},
"objectProviders": {
"id": "http://winery.napajs.org/engine-config/objectProviders",
"type": "array",
"title": "Object providers definition files.",
"description": "Optional. Object providers are functions that provide objects by URI.",
"name": "objectProviders",
"items": {
"id": "http://winery.napajs.org/engine-config/objectProviders/0",
"type": "string",
"title": "Path of an object provider definition file.",
"description": "Multiple object provider definition file is supported."
}
}
},
"additionalProperties": false,
"required": [
"allowPerRequestOverride",
"throwExceptionOnError",
"defaultExecutionStack",
"objectTypes",
"namedObjects"
]
}

Просмотреть файл

@ -0,0 +1,66 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/counter-config",
"type": "array",
"title": "Counter config schema.",
"description": "Counter config file is a sub file of application config in Napa.",
"name": "/",
"items": {
"id": "http://winery.napajs.org/counter-config/item",
"type": "object",
"title": "Counter definition schema.",
"description": "An explanation about the puropose of this instance described by this schema.",
"name": "item",
"properties": {
"name": {
"id": "http://winery.napajs.org/counter-config/item/name",
"type": "string",
"title": "Name schema.",
"description": "Counter variable name accessed via app.counters.<counterName>. Please use identifier for this property.",
"name": "name"
},
"displayName": {
"id": "http://winery.napajs.org/counter-config/item/displayName",
"type": "string",
"title": "Display name schema.",
"description": "Display name for the counter, which will be displayed in monitoring tools.",
"name": "displayName"
},
"description": {
"id": "http://winery.napajs.org/counter-config/item/description",
"type": "string",
"title": "Description schema.",
"description": "Description of this counter.",
"name": "description"
},
"type": {
"id": "http://winery.napajs.org/counter-config/item/type",
"enum": [ "Number", "Rate", "Percentile" ],
"title": "Counter type schema.",
"description": "An explanation about the puropose of this instance described by this schema.",
"name": "type"
},
"dimensionNames": {
"id": "http://winery.napajs.org/napa-config/global/counters/dimensionNames",
"type": "array",
"title": "Dimension Names",
"description": "List of dimension names being recorded with every counter sample.",
"name": "dimensionNames",
"items": {
"id": "http://winery.napajs.org/napa-config/global/counters/dimensionNames/item",
"type": "string",
"title": "Dimension name.",
"description": "Dimension name metric.",
"name": "item"
}
}
},
"additionalProperties": false,
"required": [
"name",
"displayName",
"type",
"dimensionNames"
]
}
}

Просмотреть файл

@ -0,0 +1,57 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/named-object-config",
"type": "array",
"title": "Named object config schema.",
"description": "Named object config as a sub-file of app config in Napa.",
"name": "/",
"items": {
"id": "http://winery.napajs.org/named-object-config/item",
"type": "object",
"title": "Named object definition schema.",
"description": "Named object definition.",
"name": "item",
"properties": {
"name": {
"id": "http://winery.napajs.org/named-object-config/item/name",
"type": "string",
"title": "Name schema.",
"description": "Well-known name in current application.",
"name": "name"
},
"override": {
"id": "http://winery.napajs.org/named-object-config/item/override",
"type": "boolean",
"title": "Override schema.",
"description": "If this named object will override the object with the same name declared before this definition.",
"name": "override",
"default": false
},
"description": {
"id": "http://winery.napajs.org/named-object-config/item/description",
"type": "string",
"title": "Description schema.",
"description": "Description of this object.",
"name": "description"
},
"private": {
"id": "http://winery.napajs.org/named-object-config/item/private",
"type": "boolean",
"title": "Private schema.",
"description": "If this object is private.",
"name": "private"
},
"value": {
"id": "http://winery.napajs.org/named-object-config/item/value",
"title": "Value schema.",
"description": "Value of current object, can be any JS object, or object with _type property or URI.",
"name": "value"
}
},
"additionalProperties": false,
"required": [
"name",
"value"
]
}
}

Просмотреть файл

@ -0,0 +1,75 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/object-provider-config",
"type": "array",
"title": "Object provider config Schema.",
"description": "Object provider config as a sub file of app config in Napa.",
"name": "/",
"items": {
"id": "http://winery.napajs.org/object-provider-config/item",
"type": "object",
"title": "Object provider definition schema.",
"description": "Object provider definition describes protocol name and object loading function.",
"name": "item",
"properties": {
"protocol": {
"id": "http://winery.napajs.org/object-provider-config/item/protocol",
"type": "string",
"title": "Protocol schema.",
"description": "Protocol name that current provider is responsible.",
"name": "protocol"
},
"override": {
"id": "http://winery.napajs.org/object-provider-config/item/override",
"type": "boolean",
"title": "Override schema.",
"description": "If this provider will override the provider for the same protocol name declared before this definition.",
"name": "override",
"default": false
},
"description": {
"id": "http://winery.napajs.org/object-provider-config/item/description",
"type": "string",
"title": "Description schema.",
"description": "Description of current protocol.",
"name": "description"
},
"exampleUri": {
"id": "http://winery.napajs.org/object-provider-config/item/exampleUri",
"type": "array",
"title": "Example URI.",
"description": "Example of URIs",
"name": "exampleUri",
"items": {
"id": "http://winery.napajs.org/object-provider-config/item/exampleUri/item",
"type": "string",
"title": "example Uri item.",
"description": "Example Uri for this protocol.",
"name": "item"
}
},
"moduleName": {
"id": "http://winery.napajs.org/object-provider-config/item/moduleName",
"type": "string",
"title": "Module name schema.",
"description": "Module name for the provider.",
"name": "moduleName"
},
"functionName": {
"id": "http://winery.napajs.org/object-provider-config/item/functionName",
"type": "string",
"title": "Function name schema.",
"description": "Function name for the provisioning function.",
"name": "functionName"
}
},
"additionalProperties": false,
"required": [
"protocol",
"description",
"exampleUri",
"moduleName",
"functionName"
]
}
}

Просмотреть файл

@ -0,0 +1,76 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/object-type-config",
"type": "array",
"title": "Type config schema in Napa.",
"description": "Type config is a sub config file under app config in Napa.",
"name": "/",
"items": {
"id": "http://winery.napajs.org/object-type-config/item",
"type": "object",
"title": "Type definition schema.",
"description": "Type definition describes type name and where the constructor locates.",
"name": "item",
"properties": {
"typeName": {
"id": "http://winery.napajs.org/object-type-config/item/typeName",
"type": "string",
"title": "Type schema.",
"description": "Type name, case-sensitive.",
"name": "typeName"
},
"override": {
"id": "http://winery.napajs.org/object-type-config/item/override",
"type": "boolean",
"title": "Override schema.",
"description": "If this type will override the same type name declared before this definition..",
"name": "override",
"default": false
},
"description": {
"id": "http://winery.napajs.org/object-type-config/item/description",
"type": "string",
"title": "Description schema.",
"description": "Description of the type.",
"name": "description"
},
"moduleName": {
"id": "http://winery.napajs.org/object-type-config/item/moduleName",
"type": "string",
"title": "Module name schema.",
"description": "Module name of the constructor.",
"name": "moduleName"
},
"functionName": {
"id": "http://winery.napajs.org/object-type-config/item/functionName",
"type": "string",
"title": "Function name schema.",
"description": "Function name as constructor.",
"name": "functionName"
},
"schema": {
"id": "http://winery.napajs.org/object-type-config/item/schema",
"type": "string",
"title": "JSON Schema for value element.",
"description": "JSON schema for value element.",
"name": "schema"
},
"exampleObjects": {
"id": "http://winery.napajs.org/provider-config/item/exampleObjects",
"type": "array",
"title": "Example objects schema.",
"description": "Example objects for current type.",
"name": "exampleObjects",
"items": {}
}
},
"additionalProperties": false,
"required": [
"typeName",
"description",
"moduleName",
"functionName",
"exampleObjects"
]
}
}

198
schema/request.schema.json Normal file
Просмотреть файл

@ -0,0 +1,198 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/request",
"type": "object",
"title": "Schema for Napa application request.",
"description": "Schema for Napa application request.",
"name": "/",
"properties": {
"application": {
"id": "http://winery.napajs.org/request/application",
"type": "string",
"title": "Application name schema.",
"description": "Application name. Case insensitive.",
"name": "application"
},
"entryPoint": {
"id": "http://winery.napajs.org/request/entryPoint",
"type": "string",
"title": "EntryPoint name schema.",
"description": "Entrypoint name defined as named object. Case sensitive.",
"name": "entryPoint"
},
"traceId": {
"id": "http://winery.napajs.org/request/traceId",
"type": "string",
"title": "TraceId string.",
"description": "Trace ID used for debugging purpose.",
"name": "traceId"
},
"input": {
"id": "http://winery.napajs.org/request/input",
"title": "Entry point input.",
"description": "Input JS value to entry point.",
"name": "input"
},
"controlFlags": {
"id": "http://winery.napajs.org/request/controlFlags",
"type": "object",
"title": "Control flags.",
"description": "Control flags for current request.",
"name": "controlFlags",
"properties": {
"debug": {
"id": "http://winery.napajs.org/request/controlFlags/debug",
"type": "boolean",
"title": "Debug flag.",
"description": "Enable debug or not.",
"name": "debug",
"default": false
},
"perf": {
"id": "http://winery.napajs.org/request/controlFlags/perf",
"type": "boolean",
"title": "Performance measure flag.",
"description": "Enable performance measure or not.",
"name": "perf",
"default": false
}
},
"additionalProperties": false
},
"overrideTypes": {
"id": "http://winery.napajs.org/request/overrideTypes",
"type": "array",
"title": "Override types.",
"description": "Override types for current request. That will redirect creation of object of this type to customized function and invalidate registered named object with the type when calling getNamedObject.",
"name": "overrideTypes",
"items": {
"id": "http://winery.napajs.org/request/overrideTypes/item",
"type": "object",
"title": "Override type definition",
"description": "Override type definition",
"name": "0",
"properties": {
"typeName": {
"id": "http://winery.napajs.org/request/overrideTypes/item/typeName",
"type": "string",
"title": "Type name.",
"description": "Type name to override.",
"name": "typeName"
},
"description": {
"id": "http://winery.napajs.org/request/overrideTypes/item/description",
"type": "string",
"title": "Description.",
"description": "Description for the type. Optional.",
"name": "description"
},
"moduleName": {
"id": "http://winery.napajs.org/request/overrideTypes/item/moduleName",
"type": "string",
"title": "Module name.",
"description": "Module name for the overridden type.",
"name": "moduleName"
},
"functionName": {
"id": "http://winery.napajs.org/request/overrideTypes/item/functionName",
"type": "string",
"title": "Function name used as constructor.",
"description": "Function name within the module for the overridden type.",
"name": "functionName"
}
},
"required": [ "typeName", "moduleName", "functionName" ],
"additionalProperties": false
}
},
"overrideProviders": {
"id": "http://winery.napajs.org/request/overrideProviders",
"type": "array",
"title": "Override object providers.",
"description": "Override object providers for specific URIs. This will redirect URI based object creation to customized function and invalidate registered named object referencing objects of this protocol when calling getNamedObject.",
"name": "overrideProviders",
"items": {
"id": "http://winery.napajs.org/request/overrideProviders/item",
"type": "object",
"title": "Override provider definition.",
"description": "Definition of an overridden object provider.",
"name": "item",
"properties": {
"protocol": {
"id": "http://winery.napajs.org/request/overrideProviders/item/protocol",
"type": "string",
"title": "Protocol.",
"description": "Protocol of the provider.",
"name": "protocol"
},
"description": {
"id": "http://winery.napajs.org/request/overrideProviders/item/description",
"type": "string",
"title": "Description.",
"description": "Description of the provider. Optional.",
"name": "description"
},
"moduleName": {
"id": "http://winery.napajs.org/request/overrideProviders/item/moduleName",
"type": "string",
"title": "Module name",
"description": "Module name of the provider.",
"name": "moduleName"
},
"functionName": {
"id": "http://winery.napajs.org/request/overrideProviders/item/functionName",
"type": "string",
"title": "Function name.",
"description": "Function name of the provider to load objects.",
"name": "functionName"
}
},
"required": [ "protocol", "moduleName", "functionName" ],
"additionalProperties": false
}
},
"overrideObjects": {
"id": "http://winery.napajs.org/request/overrideObjects",
"type": "array",
"title": "Override named objects.",
"description": "Override a list of named objects. This will create named object with a new name or invalidate registered named object with the type when calling getNamedObject.",
"name": "overrideObjects",
"items": {
"id": "http://winery.napajs.org/request/overrideObjects/item",
"type": "object",
"title": "Override named object definition.",
"description": "Override named object definition.",
"name": "item",
"properties": {
"name": {
"id": "http://winery.napajs.org/request/overrideObjects/item/name",
"type": "string",
"title": "Name.",
"description": "Name of the override object.",
"name": "name"
},
"description": {
"id": "http://winery.napajs.org/request/overrideObjects/item/description",
"type": "string",
"title": "Description.",
"description": "Description of the named object. Optional.",
"name": "description"
},
"value": {
"id": "http://winery.napajs.org/request/overrideObjects/item/value",
"title": "Value.",
"description": "Value of override named objects.",
"name": "value"
}
},
"required": [ "name", "value" ],
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": [
"application",
"entryPoint"
]
}

153
schema/response.schema.json Normal file
Просмотреть файл

@ -0,0 +1,153 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://winery.napajs.org/response",
"type": "object",
"title": "Napa response schema.",
"description": "Napa Response schema.",
"name": "/",
"properties": {
"responseCode": {
"id": "http://winery.napajs.org/response/responseCode",
"type": "integer",
"title": "ResponseCode schema.",
"description": "Response code.",
"name": "responseCode"
},
"errorMessage": {
"id": "http://winery.napajs.org/response/errorMessage",
"type": "string",
"title": "ErrorMessage schema.",
"description": "Error message if request failed.",
"name": "errorMessage"
},
"output": {
"id": "http://winery.napajs.org/response/output",
"title": "Output schema.",
"description": "Output object from entrypoint.",
"name": "output"
},
"debugInfo": {
"id": "http://winery.napajs.org/response/debugInfo",
"type": "object",
"title": "DebugInfo schema.",
"description": "Debug information when debug flag is on in request.",
"name": "debugInfo",
"properties": {
"exception": {
"id": "http://winery.napajs.org/response/debugInfo/exception",
"type": "object",
"title": "Exception schema.",
"description": "Exception information from response.",
"name": "exception",
"properties": {
"stack": {
"id": "http://winery.napajs.org/response/debugInfo/exception/stack",
"type": "string",
"title": "Stack schema.",
"description": "Stack trace of the exception.",
"name": "stack"
},
"message": {
"id": "http://winery.napajs.org/response/debugInfo/exception/message",
"type": "string",
"title": "Message schema.",
"description": "Message of the exception.",
"name": "message"
},
"fileName": {
"id": "http://winery.napajs.org/response/debugInfo/exception/fileName",
"type": "string",
"title": "FileName schema.",
"description": "File name from where the exception is thrown.",
"name": "fileName"
},
"lineNumber": {
"id": "http://winery.napajs.org/response/debugInfo/exception/lineNumber",
"type": "integer",
"title": "LineNumber schema.",
"description": "Line number from where the exception is thrown.",
"name": "lineNumber"
},
"columnNumber": {
"id": "http://winery.napajs.org/response/debugInfo/exception/columnNumber",
"type": "integer",
"title": "ColumnNumber schema.",
"description": "Column number from where the exception is thrown.",
"name": "columnNumber"
}
},
"additionalProperties": false
},
"events": {
"id": "http://winery.napajs.org/response/debugInfo/events",
"type": "array",
"title": "Events schema.",
"description": "Debug events generated from logger.",
"name": "events",
"items": {
"id": "http://winery.napajs.org/response/debugInfo/events/item",
"type": "object",
"title": "Item schema.",
"description": "An explanation about the puropose of this instance described by this schema.",
"name": "item",
"properties": {
"time": {
"id": "http://winery.napajs.org/response/debugInfo/events/item/time",
"type": "string",
"title": "Time schema.",
"description": "Time of the debugging event.",
"name": "time"
},
"logLevel": {
"id": "http://winery.napajs.org/response/debugInfo/events/item/logLevel",
"type": "string",
"title": "LogLevel schema.",
"description": "Log level of the event.",
"name": "logLevel"
},
"message": {
"id": "http://winery.napajs.org/response/debugInfo/events/0/message",
"type": "string",
"title": "Message schema.",
"description": "Message of the event.",
"name": "message"
}
},
"additionalProperties": false
}
},
"details": {
"id": "http://winery.napajs.org/response/debugInfo/details",
"type": "object",
"title": "Details schema.",
"description": "Details in key/values.",
"name": "details",
"properties": {},
"additionalProperties": true
}
},
"additionalProperties": false
},
"perfInfo": {
"id": "http://winery.napajs.org/response/perfInfo",
"type": "object",
"title": "PerfInfo schema.",
"description": "Performance numbers when perf flag is on at request.",
"name": "perfInfo",
"properties": {
"processingLatencyInMicro": {
"id": "http://winery.napajs.org/response/perfInfo/processingLatencyInMicro",
"type": "number",
"title": "Server processing time in microseconds.",
"description": "Server processing time in microseconds.",
"name": "processingLatencyInMicro"
}
},
"additionalProperties": true
}
},
"additionalProperties": false,
"required": [
"responseCode"
]
}

138
test/app-test.ts Normal file
Просмотреть файл

@ -0,0 +1,138 @@
import {Application, RequestContext} from '../lib/app';
import {LocalEngine} from '../lib/engine';
import * as config from "../lib/config";
import * as builtins from '../lib/builtins';
import * as wire from '../lib/wire';
import * as objectModel from '../lib/object-model';
import * as path from 'path';
import * as napa from 'napajs';
import * as assert from 'assert';
describe('winery/app', () => {
let engine = new LocalEngine(
config.EngineConfig.fromConfig(
require.resolve('../config/engine.json')));
let app: Application = undefined;
describe('Application', () => {
it('#ctor', () => {
app = new Application(engine.objectContext,
config.ApplicationConfig.fromConfig(
engine.settings,
path.resolve(__dirname, "test-app/app.json")));
});
it('#getters', () => {
assert.equal(app.id, 'test-app');
assert.equal(Object.keys(app.metrics).length, 1);
});
it('#create', () => {
assert.equal(app.create({
_type: "TypeA",
value: 1
}), 1);
assert.strictEqual(app.create("protocolA:/abc"), "abc");
});
it('#get', () => {
assert.equal(app.get('objectA'), 1);
});
it('#getEntryPoint', () => {
assert.strictEqual(app.getEntryPoint("listEntryPoints"), builtins.entryPoints.listEntryPoints);
})
it('#getInterceptor', () => {
assert.strictEqual(app.getInterceptor("executeEntryPoint"), builtins.interceptors.executeEntryPoint);
});
it('#getExecutionStack', () => {
let stack = app.getExecutionStack('foo');
assert.equal(stack.length, 2);
assert.strictEqual(stack[0], builtins.interceptors.finalizeResponse);
assert.strictEqual(stack[1], builtins.interceptors.executeEntryPoint);
stack = app.getExecutionStack('bar');
assert.equal(stack.length, 3);
assert.strictEqual(stack[0], builtins.interceptors.logRequestResponse);
assert.strictEqual(stack[1], builtins.interceptors.finalizeResponse);
assert.strictEqual(stack[2], builtins.interceptors.executeEntryPoint);
});
});
describe('RequestContext', () => {
let context: RequestContext = undefined;
let request: wire.Request = {
application: "testApp",
entryPoint: "foo",
input: "hello world"
}
it('#ctor', () => {
context = new RequestContext(app, request);
});
it('#getters', () => {
assert.deepEqual(context.controlFlags, {
debug: false,
perf: false
});
assert.equal(context.entryPointName, "foo");
assert.equal(context.input, "hello world");
assert.equal(context.traceId, "Unknown");
assert.strictEqual(context.application, app);
assert.strictEqual(context.entryPoint, context.getEntryPoint('foo'));
assert.strictEqual(context.request, request);
});
it('#create', () => {
assert.strictEqual(context.create({_type: "TypeA", value: 1}), 1);
});
it('#get', () => {
});
it('#getNamedObject: local only', () => {
});
it('#execute', () => {
});
it('#continueExecution', () => {
});
});
describe('Debugger', () => {
it('#event', () => {
});
it('#detail', () => {
});
it('#setLastError', () => {
});
it('#getOutput', () => {
});
});
describe('RequestLogger', () => {
it('#debug', () => {
});
it('#info', () => {
});
it('#err', () => {
});
it('#warn', () => {
});
});
});

258
test/config-test.ts Normal file
Просмотреть файл

@ -0,0 +1,258 @@
import * as assert from 'assert';
import * as metrics from '@napajs/metrics';
import * as config from '../lib/config';
import * as app from '../lib/app';
import * as engine from '../lib/engine';
import * as path from 'path';
describe('winery/config', () => {
describe('ObjectTypeConfig', () => {
it('#fromConfigObject: good config', () => {
let configObject = [
{
typeName: "TypeA",
description: "Type A",
moduleName: "module",
functionName: "function",
exampleObjects: [{
_type: "TypeA",
value: 1
}]
}
]
let defs = config.ObjectTypeConfig.fromConfigObject(configObject, true);
assert.deepEqual(defs, [
{
typeName: "TypeA",
description: "Type A",
moduleName: "module",
functionName: "function",
// Set default property.
override: false,
exampleObjects: [{
_type: "TypeA",
value: 1
}]
}
])
});
it('#fromConfigObject: not conform with schema', () => {
let configObject = [
{
// Should be typeName, missing exampleObjects
type: "TypeA",
moduleName: "module",
functionName: "function"
}
]
assert.throws(() => {
config.ObjectTypeConfig.fromConfigObject(configObject, true);
});
});
it ('#fromConfig', () => {
assert.doesNotThrow(() => {
config.ObjectTypeConfig.fromConfig(
path.resolve(__dirname, "test-app/object-types.json"));
});
});
});
describe('ObjectProviderConfig', () => {
it('#fromConfigObject: good config', () => {
let configObject = [
{
protocol: "protocolA",
description: "Protocol A",
moduleName: "module",
functionName: "function",
exampleUri: ["protocolA://abc"]
}
]
let defs = config.ObjectProviderConfig.fromConfigObject(configObject, true);
assert.deepEqual(defs, [
{
protocol: "protocolA",
description: "Protocol A",
moduleName: "module",
functionName: "function",
exampleUri: ["protocolA://abc"],
// Set default property.
override: false
}
])
});
it('#fromConfigObject: not conform with schema', () => {
let configObject = [
{
protocol: "protocolA",
// Should be moduleName, and missing exampleUri.
module: "module",
functionName: "function"
}
]
assert.throws( () => {
config.ObjectProviderConfig.fromConfigObject(configObject, true);
});
});
it ('#fromConfig', () => {
assert.doesNotThrow(() => {
config.ObjectProviderConfig.fromConfig(
path.resolve(__dirname, "test-app/object-providers.json"));
});
});
});
describe('NamedObjectConfig', () => {
it('#fromConfigObject: good config', () => {
let configObject = [
{
name: "objectA",
value: {
_type: "TypeA",
value: 1
}
},
{
name: "objectB",
value: 1
}
]
let defs = config.NamedObjectConfig.fromConfigObject(configObject, true);
assert.deepEqual(defs, [
{
name: "objectA",
value: {
_type: "TypeA",
value: 1
},
// Set default values.
override: false,
private: false
},
{
name: "objectB",
value: 1,
override: false,
private: false
}
])
});
it('#fromConfigObject: not conform with schema', () => {
let configObject = [
{
name: "objectA",
// Should be value.
valueDef: 1
}
]
assert.throws( () => {
config.NamedObjectConfig.fromConfigObject(configObject, true);
});
});
it ('#fromConfig', () => {
assert.doesNotThrow(() => {
config.NamedObjectConfig.fromConfig(
path.resolve(__dirname, "test-app/objects.json"));
});
});
});
describe('MetricConfig', () => {
it('#fromConfigObject: good config', () => {
let configObject = [
{
name: "myCounter1",
displayName: "My counter1",
description: "Counter description",
type: "Percentile",
dimensionNames: ["d1", "d2"]
},
{
name: "myCounter2",
displayName: "My counter2",
description: "Counter description",
type: "Rate",
dimensionNames: []
},
{
name: "myCounter3",
displayName: "My counter3",
description: "Counter description",
type: "Number",
dimensionNames: []
}
]
let defs = config.MetricConfig.fromConfigObject("DefaultSection", configObject);
assert.deepEqual(defs, [
{
name: "myCounter1",
sectionName: "DefaultSection",
displayName: "My counter1",
description: "Counter description",
type: metrics.MetricType.Percentile,
dimensionNames: ["d1", "d2"]
},
{
name: "myCounter2",
sectionName: "DefaultSection",
displayName: "My counter2",
description: "Counter description",
type: metrics.MetricType.Rate,
dimensionNames: []
},
{
name: "myCounter3",
sectionName: "DefaultSection",
displayName: "My counter3",
description: "Counter description",
type: metrics.MetricType.Number,
dimensionNames: []
}
])
});
it ('#fromConfig', () => {
assert.doesNotThrow(() => {
config.MetricConfig.fromConfig("DefaultSection",
path.resolve(__dirname, "test-app/metrics.json"));
});
});
});
let engineSettings: engine.EngineSettings = undefined;
describe('EngineConfig', () => {
it('#fromConfig', () => {
engineSettings = config.EngineConfig.fromConfig(
require.resolve("../config/engine.json"));
assert.equal(engineSettings.allowPerRequestOverride, true);
assert.deepEqual(engineSettings.defaultExecutionStack, [
"finalizeResponse",
"executeEntryPoint"
]);
assert.equal(engineSettings.baseDir, path.dirname(require.resolve('../config/engine.json')));
assert.equal(engineSettings.throwExceptionOnError, true);
});
});
describe('ApplicationConfig', () => {
let appSettings: app.ApplicationSettings = undefined;
it('#fromConfig', () => {
appSettings = config.ApplicationConfig.fromConfig(engineSettings,
path.resolve(__dirname, "test-app/app.json"));
});
it('#getters', () => {
assert.equal(appSettings.metrics.length, 1);
})
});
});

Просмотреть файл

@ -0,0 +1,15 @@
// This JSON file follows the schema ./test-json-schema.json.
{
"stringProp": "hi",
"numberProp": 0,
"booleanProp": true,
"arrayProp": [
1,
2
],
// objectProp can have additional properties.
"objectProp": {
"field1": 1,
"additionalField": "additional"
}
}

Просмотреть файл

@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"additionalProperties": false,
"id": "http://example.com/root.json",
"properties": {
"arrayProp": {
"id": "arrayProp",
"items": {
"default": 1,
"description": "An explanation about the purpose of this instance.",
"id": "0",
"title": "The 0 schema.",
"type": "integer"
},
"type": "array"
},
"booleanProp": {
"default": true,
"description": "An explanation about the purpose of this instance.",
"id": "booleanProp",
"title": "The Booleanprop schema.",
"type": "boolean"
},
"numberProp": {
"default": 0,
"description": "An explanation about the purpose of this instance.",
"id": "numberProp",
"title": "The Numberprop schema.",
"type": "integer"
},
"objectProp": {
"additionalProperties": true,
"id": "objectProp",
"properties": {
"field1": {
"default": 1,
"description": "An explanation about the purpose of this instance.",
"id": "field1",
"title": "The Field1 schema.",
"type": "integer"
}
},
"required": [
"field1"
],
"type": "object"
},
"stringProp": {
"default": "hi",
"description": "An explanation about the purpose of this instance.",
"id": "stringProp",
"title": "The Stringprop schema.",
"type": "string"
}
},
"required": [
"booleanProp",
"arrayProp",
"objectProp",
"stringProp",
"numberProp"
],
"type": "object"
}

Просмотреть файл

@ -0,0 +1,12 @@
<root>
<stringProp>hi</stringProp>
<numberProp>0</numberProp>
<booleanProp>true</booleanProp>
<arrayProp>
<item>1</item>
<item>2</item>
</arrayProp>
<objectProp>
<field1>1</field1>
</objectProp>
</root>

213
test/engine-test.ts Normal file
Просмотреть файл

@ -0,0 +1,213 @@
import * as app from '../lib/app';
import * as config from "../lib/config";
import * as builtins from '../lib/builtins';
import * as wire from '../lib/wire';
import { Engine, LocalEngine, RemoteEngine, EngineHub } from '../lib/engine';
import * as path from 'path';
import * as napa from 'napajs';
import * as assert from 'assert';
describe('winery/engine', () => {
describe('LocalEngine', () => {
let engine: LocalEngine = undefined;
it('#ctor', () => {
engine = new LocalEngine(
config.EngineConfig.fromConfig(
require.resolve('../config/engine.json'))
);
});
it('#register: success', () => {
engine.register(
path.resolve(__dirname, './test-app'),
["testApp"]);
});
it('#register: fail - duplicated instance name', () => {
assert.throws(() => {
engine.register(
path.resolve(__dirname, './test-app'),
["testApp"]);
});
});
it('#register: fail - register for another container.', () => {
assert.throws(() => {
engine.register(
path.resolve(__dirname, './test-app'),
["testApp"],
napa.createZone('zone1'));
});
});
it('#serve: sync entrypoint', (done) => {
engine.serve({
application: "testApp",
entryPoint: "foo",
input: "hello world"
}).then((response: wire.Response) => {
assert.equal(response.responseCode, wire.ResponseCode.Success);
assert.equal(response.output, 'hello world');
done();
});
});
it('#serve: async entrypoint', (done) => {
engine.serve({
application: "testApp",
entryPoint: "bar",
input: "hello world"
}).then((response: wire.Response) => {
assert.equal(response.responseCode, wire.ResponseCode.Success);
assert.equal(response.output, "hello world");
done();
})
});
it('#serve: bad request - malformat JSON ', (done) => {
engine.serve(`{
"application": "testApp",
"entryPoint": "foo",
}`).catch((error: Error) => {
done(error.message === "Unexpected token }. Fail to parse request string."
? undefined : error);
});
});
it('#serve: bad request - not registered application ', (done) => {
engine.serve({
application: "testApp2",
entryPoint: "foo"
}).catch((error: Error) => {
done(error.message === "'testApp2' is not a known application"? undefined: error);
});
});
it('#serve: bad request - entryPoint not found ', (done) => {
engine.serve({
application: "testApp",
entryPoint: "foo2"
}).catch((error: Error) => {
done(error.message === "Entrypoint does not exist: 'foo2'" ? undefined: error);
});
});
it('#serve: application throws exception ', (done) => {
engine.serve({
application: "testApp",
entryPoint: "alwaysThrow"
}).catch((error) => {
done(error.message === "You hit an always-throw entrypoint."? undefined: error);
});
});
it('#applicationInstanceNames', () => {
assert.deepEqual(engine.applicationInstanceNames, ["testApp"]);
});
});
describe.skip('RemoteEngine', () => {
let engine: RemoteEngine = undefined;
let zone: napa.Zone = napa.createZone('zone2');
it('#ctor', () => {
engine = new RemoteEngine(zone);
});
it('#register: success', () => {
engine.register(
path.resolve(__dirname, './test-app'),
["testApp"]);
});
it('#register: fail - duplicated instance name', () => {
assert.throws(() => {
engine.register(
path.resolve(__dirname, './test-app'),
["testApp"]);
});
});
it('#register: fail - register for another container.', () => {
assert.throws(() => {
engine.register(
path.resolve(__dirname, './test-app'),
["testApp"],
napa.createZone('zone3'));
});
});
it('#serve: sync entrypoint', (done) => {
engine.serve({
application: "testApp",
entryPoint: "foo",
input: "hello world"
}).then((response: wire.Response) => {
assert.equal(response.responseCode, wire.ResponseCode.Success);
assert.equal(response.output, 'hello world');
done();
});
});
it('#serve: async entrypoint', (done) => {
engine.serve({
application: "testApp",
entryPoint: "bar",
input: "hello world"
}).then((response: wire.Response) => {
assert.equal(response.responseCode, wire.ResponseCode.Success);
assert.equal(response.output, "hello world");
done();
})
});
it('#serve: bad request - malformat JSON ', (done) => {
engine.serve(`{
"application": "testApp",
"entryPoint": "foo",
}`).catch((error: Error) => {
done(error.message === "Unexpected token }. Fail to parse request string."
? undefined : error);
});
});
it('#serve: bad request - not registered application ', (done) => {
engine.serve({
application: "testApp2",
entryPoint: "foo"
}).catch((error: Error) => {
done(error.message === "'testApp2' is not a known application"? undefined: error);
});
});
it('#serve: bad request - entryPoint not found ', (done) => {
engine.serve({
application: "testApp",
entryPoint: "foo2"
}).catch((error: Error) => {
done(error.message === "Entrypoint does not exist: 'foo2'" ? undefined: error);
});
});
it('#serve: application throws exception ', (done) => {
engine.serve({
application: "testApp",
entryPoint: "alwaysThrow"
}).catch((error) => {
done(error.message === "You hit an always-throw entrypoint."? undefined: error);
});
});
it('#applicationInstanceNames', () => {
assert.deepEqual(engine.applicationInstanceNames, ["testApp"]);
});
});
// TODO: @dapeng, implement after RemoteEngine is ready.
describe("EngineHub", () => {
it('#register: local');
it('#register: remote');
it('#serve: local');
it('#serve: remote');
});
});

57
test/named-object-test.ts Normal file
Просмотреть файл

@ -0,0 +1,57 @@
import * as assert from 'assert';
import * as objectModel from '../lib/object-model';
describe('winery/named-object', () => {
describe('NamedObjectRegistry', () => {
let collection = new objectModel.NamedObjectRegistry();
let objectA: objectModel.NamedObject = {
scope: "global",
definition: {
name: "objectA",
value: 1
},
value: 1
}
it('#has', () => {
assert(!collection.has('objectA'));
});
it('#insert', () => {
collection.insert(objectA);
assert(collection.has('objectA'));
});
it('#get', () => {
let output = collection.get("objectA")
assert.strictEqual(output, objectA);
});
it('#forEach', () => {
collection.forEach((object: objectModel.NamedObject) => {
assert.strictEqual(object, objectA);
});
})
it('#fromDefinition', () => {
let objectContext = {
create: (input: any): any => {
return input;
},
get: (name: string): objectModel.NamedObject => {
return null;
},
forEach: (callback: (object: objectModel.NamedObject) => void) => {
// Do nothing.
},
baseDir: __dirname
}
collection = objectModel.NamedObjectRegistry.fromDefinition(
"global",
[{ name: "objectA", value: 1 }],
objectContext);
assert(collection.has('objectA'));
assert.deepEqual(collection.get('objectA'), objectA);
})
});
});

277
test/object-context-test.ts Normal file
Просмотреть файл

@ -0,0 +1,277 @@
import * as assert from 'assert';
import * as objectModel from '../lib/object-model';
describe('winery/object-context', () => {
let perAppDef: objectModel.ScopedObjectContextDefinition = null;
let perRequestDef: objectModel.ScopedObjectContextDefinition = null;
// Test suite for ScopedObjectContextDefinition.
describe('ScopedObjectContextDefinition', () => {
// Per app definitions.
let perAppTypeDefs: objectModel.TypeDefinition[] = [
{
typeName: "TypeA",
moduleName: "./object-context-test",
functionName: "types.createTypeA"
},
{
typeName: "TypeB",
moduleName: "./object-context-test",
functionName: "types.createTypeB"
}
];
let perAppProviderDefs: objectModel.ProviderDefinition[] = [
{
protocol: "ProtocolA",
moduleName: "./object-context-test",
functionName: "provideProtocolA"
}
];
let perAppObjectDefs: objectModel.NamedObjectDefinition[] = [
{
name: "objectA",
value: {
_type: "TypeA",
value: 1
}
},
{
name: "objectB",
value: {
_type: "TypeB",
value: {
_type: "TypeA",
value: 1
}
}
},
{
name: "objectC",
value: "ProtocolA://abc"
},
];
// Per request definitions.
let perRequestTypeDefs: objectModel.TypeDefinition[] = [
{
typeName: "TypeA",
moduleName: "./object-context-test",
functionName: "types.createTypeAPrime"
}
];
let perRequestProviderDefs: objectModel.ProviderDefinition[] = [
{
protocol: "ProtocolA",
moduleName: "./object-context-test",
functionName: "provideProtocolAPrime"
}
];
let perRequestObjectDefs: objectModel.NamedObjectDefinition[] = [
{
name: "objectA",
value: {
_type: "TypeA",
value: 2
}
},
{
name: "objectC",
value: "ProtocolA://cde"
},
{
name: "objectD",
value: "ProtocolA://def"
}
];
it("#ctor", () => {
assert.doesNotThrow(() => {
perAppDef = new objectModel.ScopedObjectContextDefinition(null, perAppTypeDefs, perAppProviderDefs, perAppObjectDefs, true);
perRequestDef = new objectModel.ScopedObjectContextDefinition(perAppDef, perRequestTypeDefs, perRequestProviderDefs, perRequestObjectDefs, false);
});
});
it('#getters', () => {
assert.strictEqual(perAppDef.parent, null);
assert.strictEqual(perAppDef.types, perAppTypeDefs);
assert.strictEqual(perAppDef.providers, perAppProviderDefs);
assert.strictEqual(perAppDef.namedObjects, perAppObjectDefs);
assert.strictEqual(perAppDef.getType('TypeA'), perAppTypeDefs[0]);
assert.strictEqual(perAppDef.getType('TypeB'), perAppTypeDefs[1]);
assert.strictEqual(perAppDef.getProvider('ProtocolA'), perAppProviderDefs[0]);
assert.strictEqual(perAppDef.getNamedObject('objectA'), perAppObjectDefs[0]);
assert.strictEqual(perAppDef.getNamedObject('objectB'), perAppObjectDefs[1]);
assert.strictEqual(perAppDef.getNamedObject('objectC'), perAppObjectDefs[2]);
});
it('#analyzeDependency', () => {
// objectA
let dep1 = perAppObjectDefs[0].dependencies;
assert(dep1.objectDependencies.size == 0);
assert(dep1.protocolDependencies.size == 0);
assert(dep1.typeDependencies.size == 1 && dep1.typeDependencies.has('TypeA'));
// objectB
let dep2 = perAppObjectDefs[1].dependencies;
assert(dep2.objectDependencies.size == 0);
assert(dep2.protocolDependencies.size == 0);
assert(dep2.typeDependencies.size == 2
&& dep2.typeDependencies.has('TypeA')
&& dep2.typeDependencies.has('TypeB'));
// objectC
let dep3 = perAppObjectDefs[2].dependencies;
assert(dep3.objectDependencies.size == 0);
assert(dep3.protocolDependencies.size == 1 && dep3.protocolDependencies.has('ProtocolA'));
assert(dep3.typeDependencies.size == 0);
});
});
// Test suite for ScopedObjectContext
describe('ScopedObjectContext', () => {
let perAppContext: objectModel.ScopedObjectContext = null;
let perRequestContext: objectModel.ScopedObjectContext = null;
it('#ctor', () => {
perAppContext = new objectModel.ScopedObjectContext("application", __dirname, null, perAppDef);
perRequestContext = new objectModel.ScopedObjectContext("request", __dirname, perAppContext, perRequestDef);
});
it('#getters', () => {
assert.strictEqual(perAppContext.scope, "application");
assert.strictEqual(perAppContext.baseDir, __dirname);
assert.strictEqual(perAppContext.definition, perAppDef);
assert.strictEqual(perAppContext.parent, null);
assert.strictEqual(perRequestContext.parent, perAppContext);
});
it('#create: overridden TypeA', () => {
let inputA = { _type: "TypeA", value: 1};
assert.strictEqual(perAppContext.create(inputA), 1);
assert.strictEqual(perRequestContext.create(inputA), 2);
});
it('#create: not overridden TypeB', () => {
let inputB = { _type: "TypeB", value: { _type: "TypeA", value: 1}};
assert.strictEqual(perAppContext.create(inputB), 1);
// B returns A's value, which is different from per-app and per-request.
assert.strictEqual(perRequestContext.create(inputB), 2);
});
it('#create: overridden ProtocolA', () => {
let uri = "ProtocolA://abc";
assert.strictEqual(perAppContext.create(uri), "/abc");
assert.strictEqual(perRequestContext.create(uri), "/abc*");
});
it('#get: overriden objectA', () => {
let objectA = perAppContext.get('objectA');
assert.strictEqual(objectA.scope, 'application');
assert.strictEqual(objectA.value, 1);
objectA = perRequestContext.get('objectA');
assert.strictEqual(objectA.scope, 'request');
assert.strictEqual(objectA.value, 3);
});
it('#get: not overridden objectB', () => {
let objectB = perAppContext.get('objectB');
assert.strictEqual(objectB.scope, 'application');
assert.strictEqual(objectB.value, 1);
objectB = perRequestContext.get('objectB');
assert.strictEqual(objectB.scope, 'application');
assert.strictEqual(objectB.value, 1);
});
it('#get: overriden objectC with new providerA', () => {
let objectC = perAppContext.get('objectC');
assert.strictEqual(objectC.scope, 'application');
assert.strictEqual(objectC.value, '/abc');
objectC = perRequestContext.get('objectC');
assert.strictEqual(objectC.scope, 'request');
assert.strictEqual(objectC.value, '/cde*');
});
it('#get: new objectD with new providerA', () => {
let objectD = perAppContext.get('objectD');
assert(objectD == null);
objectD = perRequestContext.get('objectD');
assert.strictEqual(objectD.scope, 'request');
assert.strictEqual(objectD.value, '/def*');
});
it('#forEach: without parent scope', () => {
let objectNames: string[] = []
perAppContext.forEach(object => {
objectNames.push(object.definition.name);
});
assert.strictEqual(objectNames.length, 3);
assert(objectNames.indexOf('objectA') >= 0);
assert(objectNames.indexOf('objectB') >= 0);
assert(objectNames.indexOf('objectC') >= 0);
});
it('#forEach: with parent scope', () => {
let objectCount = 0;
let objectByName = new Map<string, objectModel.NamedObject>();
perRequestContext.forEach(object => {
++objectCount;
objectByName.set(object.definition.name, object);
if (object.definition.name !== 'objectB') {
assert(object.scope === 'request');
}
});
assert.strictEqual(objectCount, 4);
assert.strictEqual(objectByName.size, objectCount);
assert(objectByName.has('objectA') && objectByName.get('objectA').scope === 'request');
assert(objectByName.has('objectB') && objectByName.get('objectB').scope === 'application');
assert(objectByName.has('objectC') && objectByName.get('objectC').scope === 'request');
assert(objectByName.has('objectD') && objectByName.get('objectD').scope === 'request');
});
// TODO: Add test for needsUpdate.
it('#needsUpdate');
});
});
export type TypeAInput = { _type: "TypeA", value: number};
// Test calling function with
export namespace types {
export function createTypeA(input: TypeAInput | TypeAInput[]) {
if (Array.isArray(input)) {
return input.map(elem => elem.value);
}
return input.value;
}
export function createTypeAPrime(input: TypeAInput | TypeAInput[]) {
if (Array.isArray(input)) {
return input.map(elem => elem.value + 1);
}
return input.value + 1;
}
export function createTypeB(input: TypeBInput, context: objectModel.ObjectContext) {
return context.create(input.value);
}
}
export type TypeBInput = { _type: "TypeB", value: TypeAInput};
export function provideProtocolA(uri: objectModel.Uri): any {
return uri.path;
}
export function provideProtocolAPrime(uri: objectModel.Uri): any {
return uri.path + '*';
}

Просмотреть файл

@ -0,0 +1,138 @@
import * as assert from 'assert';
import * as path from 'path';
import * as objectModel from '../lib/object-model';
describe('winery/object-provider', () => {
describe('Uri', () => {
it('#parse: absolute path with no parameters.', () => {
let uri = objectModel.Uri.parse("doc://a/d/e/f");
assert.equal(uri.path, "/a/d/e/f");
assert.equal(uri.protocol, "doc");
});
it('#parse: relative path with no parameters.', () => {
let uri = objectModel.Uri.parse("doc:/a/d/e/f");
assert.equal(uri.path, "a/d/e/f");
assert.equal(uri.protocol, "doc");
});
it('#parse: absolute path with parameters.', () => {
let uri = objectModel.Uri.parse("doc://a/d/e/f?a=1&b=2");
assert.equal(uri.path, "/a/d/e/f");
assert.equal(uri.protocol, "doc");
assert.strictEqual(uri.getParameter("a"), "1");
assert.strictEqual(uri.getParameter("b"), "2");
});
it('#parse: bad format.', () => {
assert.throws(() => {
objectModel.Uri.parse("doc//a/d/e/f?a=1&b=2");
});
});
});
describe('ProviderRegistry', () => {
let provider = new objectModel.ProviderRegistry();
// ProtocolA support both a single element and an array as input.
it('#register', () => {
provider.register('protocolA',
(uri: objectModel.Uri | objectModel.Uri[]): string | string[] => {
if (Array.isArray(uri)) {
return uri.map(value => { return value.path; });
}
return uri.path;
});
// ProtocolB needs an ObjectContext to create inner object.
provider.register('protocolB',
(input: objectModel.Uri, context: objectModel.ObjectContext): any => {
return path.resolve(context.baseDir, input.path);
});
});
it('#supports', () => {
// Case insensitive.
assert(provider.supports('protocolA'));
assert(provider.supports('ProtocolA'));
assert(provider.supports('protocola'));
assert(provider.supports('protocolB'));
assert(!provider.supports('protocolC'));
});
it('#provide: unsupported protocol', () => {
// Create object of unsupported type.
assert.throws(() => {
provider.provide(objectModel.Uri.parse("protocolC://abc"));
},
Error);
});
let uriA1 = objectModel.Uri.parse("protocolA://abc");
let expectedA1 = "/abc";
it('#provide: input with single uri', () => {
// Create object with a single uri.
let a1 = provider.provide(uriA1);
assert.strictEqual(a1, expectedA1);
});
it('#provide: case insensitive protocol', () => {
// Create object with a single uri.
let a1 = provider.provide(objectModel.Uri.parse("PrOtOcOlA://abc"));
assert.strictEqual(a1, expectedA1);
});
it('#provide: input with array of uri.', () => {
// Create an array of objects with an array of uris.
let uriA2 = objectModel.Uri.parse("protocolA://cde");
let arrayA = provider.provide([uriA1, uriA2]);
assert.deepEqual(arrayA, ["/abc", "/cde"]);
});
// Create an object that needs ObjectContext.
// Create a simple context.
var context: objectModel.ObjectContext = {
create: (input: any): any => {
return null;
},
get: (name: string): objectModel.NamedObject => {
return null;
},
forEach: (callback: (object: objectModel.NamedObject) => void) => {
// Do nothing.
},
baseDir: __dirname
}
let uriB1 = objectModel.Uri.parse("protocolB:/file1.txt");
it('#provide: protocol needs object context', () => {
assert.equal(provider.provide(uriB1, context), path.resolve(__dirname, "file1.txt"));
});
it('#provide: mixed protocol in a Uri array', () => {
// Create an array of objects of different protocol.
assert.throws(() => {
provider.provide([uriA1, uriB1], context);
}, Error);
});
it('ProviderRegistry#fromDefinition', () => {
let defs: objectModel.ProviderDefinition[] = [{
protocol: "protocolA",
moduleName: "./object-provider-test",
functionName: "loadA"
}];
let provider = objectModel.ProviderRegistry.fromDefinition(defs, __dirname);
assert(provider.supports('protocolA'));
let uriA1 = objectModel.Uri.parse("protocolA://abc")
assert.equal(provider.provide(uriA1), "/abc");
});
});
});
export function loadA(uri: objectModel.Uri): string {
return uri.path;
}

99
test/object-type-test.ts Normal file
Просмотреть файл

@ -0,0 +1,99 @@
import * as assert from 'assert';
import * as objectModel from '../lib/object-model';
describe('winery/object-type', () => {
describe('TypeRegistry', () => {
let factory = new objectModel.TypeRegistry();
it('#register', () => {
// TypeA constructor support both a single element and an array as input.
type TypeAInput = { "_type": "TypeA", "value": number};
factory.register('TypeA',
(input: TypeAInput | TypeAInput[]): number | number[] => {
if (Array.isArray(input)) {
return input.map(value => { return value.value; });
}
return input.value;
});
// TypeB constructor needs an ObjectContext to create inner object.
factory.register('TypeB',
(input: {"_type": "TypeB", "value": objectModel.ObjectWithType}, context: objectModel.ObjectContext): any => {
return context.create(input.value);
});
});
it('#supports', () => {
assert(factory.supports('TypeA'));
assert(factory.supports('TypeB'));
assert(!factory.supports('TypeC'));
});
it('#create: unsupported type', () => {
// Create object of unsupported type.
assert.throws(() => {
factory.create({'_type': 'TypeC'})
},
Error);
});
let inputA1 = { "_type": "TypeA", "value": 1};
let expectedA1 = 1;
it('#create: input as single element', () => {
// Create object with a single element.
let a1 = factory.create(inputA1);
assert.equal(a1, expectedA1);
});
it('#create: input as array', () => {
// Create an array of objects of the same type.
let inputA2 = { "_type": "TypeA", "value": 2};
let arrayA = factory.create([inputA1, inputA2]);
assert.deepEqual(arrayA, [1, 2]);
});
// Create an object that needs ObjectContext.
// Create a simple context.
var context: objectModel.ObjectContext = {
create: (input: any): any => {
return factory.create(<objectModel.ObjectWithType>input);
},
get: (name: string): objectModel.NamedObject => {
return null;
},
forEach: (callback: (object: objectModel.NamedObject) => void) => {
// Do nothing.
},
baseDir: __dirname
}
let inputB1 = {"_type": "TypeB", "value": inputA1};
it('#create: constructor needs a context object.', () => {
assert.equal(factory.create(inputB1, context), expectedA1);
});
it('#create: array input with different types.', () => {
// Create an array of objects of different type.
assert.throws(() => {
factory.create([inputA1, inputB1], context);
}, Error);
});
it('#fromDefinition', () => {
let defs: objectModel.TypeDefinition[] = [{
typeName: "TypeA",
moduleName: "./object-type-test",
functionName: "createA"
}];
factory = objectModel.TypeRegistry.fromDefinition(defs, __dirname);
assert(factory.supports('TypeA'));
let inputA1 = { "_type": "TypeA", "value": 1};
assert.equal(factory.create(inputA1), 1);
});
});
});
export function createA(input: {_type: "TypeA", value: number}) {
return input.value;
}

29
test/test-app/app.json Normal file
Просмотреть файл

@ -0,0 +1,29 @@
{
"id": "test-app",
"description": "Test application",
"allowPerRequestOverride": true,
"defaultExecutionStack": [
"finalizeResponse",
"executeEntryPoint"
],
"objectTypes": [
"./object-types.json"
],
"objectProviders": [
"./object-providers.json"
],
"namedObjects": [
"./entrypoints.json",
"./objects.json"
],
"metrics": {
"sectionName": "TestApp",
"definition": [
"./metrics.json"
]
}
}

Просмотреть файл

@ -0,0 +1,31 @@
[
{
"name": "foo",
"value": {
"_type": "EntryPoint",
"moduleName": "./test-app",
"functionName": "entrypoints.foo"
}
},
{
"name": "bar",
"value": {
"_type": "EntryPoint",
"moduleName": "./test-app",
"functionName": "entrypoints.bar",
"executionStack": [
"logRequestResponse",
"finalizeResponse",
"executeEntryPoint"
]
}
},
{
"name": "alwaysThrow",
"value": {
"_type": "EntryPoint",
"moduleName": "./test-app",
"functionName": "entrypoints.alwaysThrow"
}
}
]

Просмотреть файл

@ -0,0 +1,9 @@
[
{
"name": "requestRateFoo",
"type": "Rate",
"displayName": "Request Rate Foo",
"description": "Request rate of foo",
"dimensionNames": []
}
]

Просмотреть файл

@ -0,0 +1,11 @@
[
{
"protocol": "ProtocolA",
"description": "Protocol A",
"moduleName": "./test-app",
"functionName": "providers.provideA",
"exampleUri": [
"ProtocolA://abcde"
]
}
]

Просмотреть файл

@ -0,0 +1,29 @@
[
{
"typeName": "TypeA",
"description": "Test type A",
"moduleName": "./test-app",
"functionName": "types.createA",
"exampleObjects": [
{
"_type": "TypeA",
"value": 1
}
]
},
{
"typeName": "TypeB",
"description": "Test type B",
"moduleName": "./test-app",
"functionName": "types.createB",
"exampleObjects": [
{
"_type": "TypeB",
"value": {
"_type": "TypeA",
"value": 1
}
}
]
}
]

Просмотреть файл

@ -0,0 +1,9 @@
[
{
"name": "objectA",
"value": {
"_type": "TypeA",
"value": 1
}
}
]

36
test/test-app/test-app.ts Normal file
Просмотреть файл

@ -0,0 +1,36 @@
import {RequestContext} from '../../lib/app';
import * as objectModel from '../../lib/object-model';
export namespace types {
export function createA(input: {_type: "TypeA", value: number}): number {
return input.value;
}
export function createB(input: {_type: "TypeB", value: any}, context: objectModel.ObjectContext): any {
return context.create(input.value);
}
}
export namespace providers {
export function provideA(uri: objectModel.Uri): string {
return uri.path;
}
}
export namespace entrypoints {
export function foo(context: RequestContext, input: string): string {
return input;
}
export function bar(context: RequestContext, input: string): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve(input);
}, 20);
});
}
export function alwaysThrow() {
throw new Error("You hit an always-throw entrypoint.");
}
}

19
test/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"noImplicitAny": true,
"removeComments": false,
"preserveConstEnums": true,
"baseUrl": ".",
"declaration": false,
"lib": [
"es2015"
],
"outDir": "objd/amd64/node_modules/winery"
},
"exclude": [
"objd",
"obj",
"**/.vscode"
]
}

410
test/utils-test.ts Normal file
Просмотреть файл

@ -0,0 +1,410 @@
import * as utils from '../lib/utils';
import * as path from 'path';
import * as assert from 'assert';
describe('winery/utils', () => {
let schema = new utils.JsonSchema(
path.resolve(__dirname, 'config/utils-test.schema.json'));
describe('JsonSchema', () => {
it('#validate: valid input', () => {
assert(schema.validate({
stringProp: "hello world",
numberProp: 0,
booleanProp: true,
arrayProp: [1, 2],
objectProp: {
field1: 1,
additionalField: "additional"
}
}));
});
it('#validate: wrong type', () => {
assert(!schema.validate({
stringProp: 1,
numberProp: 0,
booleanProp: true,
arrayProp: [1, 2],
objectProp: {
field1: 1,
additionalField: "additional"
}
}));
});
it('#validate: missing required properties', () => {
assert(!schema.validate({
numberProp: 0,
booleanProp: true,
arrayProp: [1, 2],
objectProp: {
field1: 1,
}
}));
});
it('#validate: not-allowed additional properties', () => {
assert(!schema.validate({
stringProp: "hello world",
numberProp: 0,
booleanProp: true,
notAllowedExtra: "extra",
arrayProp: [1, 2],
objectProp: {
field1: 1,
additionalField: "additional"
}
}));
});
});
describe('Json reading', () => {
it('#parseJsonString: valid string, no schema check', () => {
let value = utils.parseJsonString('{ "prop": 1 }');
assert.equal(value.prop, 1);
});
it('#parseJsonString: valid string, comform with schema', () => {
let jsonString = `
{
"stringProp": "hi",
"numberProp": 0,
"booleanProp": true,
"arrayProp": [
1,
2
],
"objectProp": {
"field1": 1
}
}
`;
let value: any = undefined;
assert.doesNotThrow(() => {
value = utils.parseJsonString(jsonString, schema);
});
assert.equal(value.stringProp, 'hi');
assert.equal(value.numberProp, 0);
assert.equal(value.booleanProp, true);
assert.deepEqual(value.arrayProp, [1, 2]);
assert.deepEqual(value.objectProp, {
field1: 1
});
});
it('#parseJsonString: valid string, not comform with schema', () => {
// Missing 'stringProp'.
let jsonString = `
{
"numberProp": 0,
"booleanProp": true,
"arrayProp": [
1,
2
],
"objectProp": {
"field1": 1
}
}
`;
assert.throws(() => {
utils.parseJsonString(jsonString, schema);
});
});
it('#parseJsonString: allow comments', () => {
let jsonString = `
{
// This is a comment.
"prop": 0
}
`;
let value: any = undefined;
//assert.doesNotThrow(() => {
value = utils.parseJsonString(jsonString, null, true);
//});
assert.equal(value.prop, 0);
});
it('#parseJsonString: not allow comments', () => {
let jsonString = `
{
// This is a comment.
"prop": 0
}
`;
assert.throws(() => {
utils.parseJsonString(jsonString, null, false);
});
});
it('#parseJsonString: invalid JSON string', () => {
let jsonString = `
{
"prop": 0,
}
`;
assert.throws(() => {
utils.parseJsonString(jsonString);
});
});
it('#parseJsonFile', () => {
let value: any = undefined;
assert.doesNotThrow(() => {
value = utils.parseJsonFile(
path.resolve(__dirname, "config/utils-test.json"),
schema,
true);
});
assert.equal(value.stringProp, 'hi');
assert.equal(value.numberProp, 0);
assert.equal(value.booleanProp, true);
assert.deepEqual(value.arrayProp, [1, 2]);
assert.deepEqual(value.objectProp, {
field1: 1,
additionalField: "additional"
});
});
});
describe('Xml reading', () => {
it('#parseXmlString: various elements', () => {
let xmlString = `
<root>
<prop1>1</prop1>
<prop2 type="string">2</prop2>
<prop3>true</prop3>
<prop4 type="string">false</prop4>
<prop5>
<item>1</item>
<item>2</item>
</prop5>
<prop6 itemElement="customItem">
<customItem>hi</customItem>
<customItem>true</customItem>
<customItem>1</customItem>
</prop6>
</root>
`;
let value: any = undefined;
assert.doesNotThrow(() => {
value = utils.parseXmlString(xmlString);
});
assert.strictEqual(typeof value, "object");
assert.strictEqual(value.prop1, 1);
assert.strictEqual(value.prop2, "2");
assert.strictEqual(value.prop3, true);
assert.strictEqual(value.prop4, "false");
assert.deepEqual(value.prop5, [1, 2]);
assert.deepEqual(value.prop6, ["hi", true, 1]);
});
it('#parseXmlString: valid format, conform with schema', () => {
let xmlString = `
<root>
<stringProp>hi</stringProp>
<numberProp>0</numberProp>
<booleanProp>true</booleanProp>
<arrayProp>
<item>1</item>
<item>2</item>
</arrayProp>
<objectProp>
<field1>1</field1>
</objectProp>
</root>
`;
let value: any = undefined;
assert.doesNotThrow(() => {
value = utils.parseXmlString(xmlString, schema);
});
assert.equal(value.stringProp, 'hi');
assert.equal(value.numberProp, 0);
assert.equal(value.booleanProp, true);
assert.deepEqual(value.arrayProp, [1, 2]);
assert.deepEqual(value.objectProp, {
field1: 1
});
});
it('#parseXmlString: valid format, not conform with schema', () => {
// Missing property 'stringProp'
let xmlString = `
<root>
<numberProp>0</numberProp>
<booleanProp>true</booleanProp>
<arrayProp>
<item>1</item>
<item>2</item>
</arrayProp>
<objectProp>
<field1>1</field1>
</objectProp>
</root>
`;
assert.throws(() => {
utils.parseXmlString(xmlString, schema);
});
});
it('#parseXmlString: invalid format', () => {
let xmlString = `
<root><root>
`;
assert.throws(() => {
utils.parseXmlString(xmlString, schema);
});
});
it('#parseXmlFile', () => {
let value: any = undefined;
assert.doesNotThrow(() => {
value = utils.parseXmlFile(
path.resolve(__dirname, "config/utils-test.xml"),
schema);
});
assert.equal(value.stringProp, 'hi');
assert.equal(value.numberProp, 0);
assert.equal(value.booleanProp, true);
assert.deepEqual(value.arrayProp, [1, 2]);
assert.deepEqual(value.objectProp, {
field1: 1
});
});
});
describe('Unified config reading', () => {
// TODO:
it('#readConfig');
});
describe('Transform', () => {
it('#RenameProperties', () => {
let value: any = {
strProp: "value",
numProp: 1
};
value = new utils.RenameProperties( {
strProp: "stringProp",
numProp: "numberProp"
}).apply(value);
assert.strictEqual(value.stringProp, "value");
assert.strictEqual(value.numberProp, 1);
});
it('#SetDefaultValue', () => {
let value: any = {
stringProp: "value"
};
value = new utils.SetDefaultValue( {
optionalProp: true
}).apply(value);
assert.strictEqual(value.optionalProp, true);
});
it('#TransformPropertyValues', () => {
let value: any = {
stringProp: "1"
};
value = new utils.TransformPropertyValues( {
stringProp: (text: string) => { return Number.parseInt(text); }
}).apply(value);
assert.strictEqual(value.stringProp, 1);
});
it('#ChainableTransform', () => {
let value: any = {
strProp: "1"
};
value = new utils.RenameProperties( { strProp: "stringProp"}).add(
new utils.SetDefaultValue( {optionalProp: true})).add(
new utils.TransformPropertyValues( {
stringProp: (text: string) => { return Number.parseInt(text);}
})).apply(value);
assert.deepEqual(value, {
stringProp: 1,
optionalProp: true
});
})
});
describe('Miscs', () => {
it('#appendMessageOnException', () => {
let extraMessage = "extra message.";
try {
utils.appendMessageOnException(extraMessage, () => {
throw new Error("intentional error.");
})
}
catch (error) {
console.log(error.message);
assert(error.message.endsWith(extraMessage));
}
});
it('#makePromiseIfNotAlready', () => {
let funcReturnsPromise = (): Promise<number> => {
return Promise.resolve(1);
};
let funcDoesnotReturnsPromise = (): number => {
return 1;
}
let ret1 = utils.makePromiseIfNotAlready(funcReturnsPromise());
ret1.then((value: number) => {
assert.equal(value, 1);
});
let ret2 = utils.makePromiseIfNotAlready(funcDoesnotReturnsPromise());
ret2.then((value: number) => {
assert.equal(value, 1);
})
});
it('#loadFunction', () => {
let moduleName = path.resolve(__dirname, 'utils-test');
let f1 = utils.loadFunction(moduleName, "func1");
assert.equal(func1, f1);
let f2 = utils.loadFunction(moduleName, 'ns.func2');
assert.equal(ns.func2, f2);
let f3 = utils.loadFunction(moduleName, 'ns.child.func3');
assert.equal(ns.child.func3, f3);
let f4 = utils.loadFunction(moduleName, 'ns.A.method');
assert.equal(ns.A.method, f4);
});
});
});
/// Functions for testing utils.loadFunction.
export function func1(): number {
return 0;
}
export namespace ns {
export function func2(): void {
}
export namespace child {
export function func3(): void {
}
}
export class A {
public static method(): void {
}
}
}