├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── config ├── builtin-entrypoints.json ├── builtin-interceptors.json ├── builtin-types.json └── host.json ├── docs ├── api │ ├── application.md │ ├── host.md │ ├── object-context.md │ ├── request-template.md │ ├── request.md │ └── response.md ├── images │ ├── application-layout.png │ ├── architecture.png │ ├── execution-stack.png │ ├── hosting.png │ ├── object-context-chain.png │ ├── request-layout.png │ └── request-template.png └── tutorial │ └── step-by-step.md ├── examples └── playground │ ├── app.json │ ├── app.ts │ ├── entrypoints.json │ ├── objects.json │ └── tsconfig.json ├── lib ├── ajv-bundle.js ├── application.ts ├── builtin-entrypoints.ts ├── builtin-interceptors.ts ├── builtin-types.ts ├── builtins.ts ├── host.ts ├── index.ts ├── metric.ts ├── named-object.ts ├── object-context.ts ├── object-model.ts ├── object-provider.ts ├── object-type.ts ├── request-context.ts ├── request-template.ts ├── request.ts ├── response.ts ├── tsconfig.json └── utils.ts ├── package.json ├── schema ├── application-config.schema.json ├── host-config.schema.json ├── metric-config.schema.json ├── named-object-config.schema.json ├── object-provider-config.schema.json ├── object-type-config.schema.json ├── request-template.schema.json ├── request.schema.json └── response.schema.json └── test ├── application-test.ts ├── config ├── utils-test.json ├── utils-test.schema.json └── utils-test.xml ├── host-test.ts ├── metric-test.ts ├── named-object-test.ts ├── object-context-test.ts ├── object-provider-test.ts ├── object-type-test.ts ├── request-context-test.ts ├── test-app ├── app.json ├── entrypoints.json ├── level0.template.json ├── level1.template.json ├── metrics.json ├── object-providers.json ├── object-types.json ├── objects.json └── test-app.ts ├── tsconfig.json └── utils-test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | types 3 | 4 | # ignore all js files under lib except explicit ones specified here 5 | lib/**/*.js 6 | !lib/*-bundle.js 7 | !lib/zone/zone-main.js 8 | !lib/binding.js 9 | 10 | # ignore all js files under test 11 | test/**/*.js 12 | 13 | # ignore all test files under examples 14 | examples/**/*.js 15 | 16 | package-lock.json 17 | 18 | !tsconfig.json 19 | 20 | # ignore all VSCode files 21 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude typescript source files 2 | **/*.ts 3 | !types/*.ts 4 | 5 | # Exclude typescript configuration 6 | tsconfig.json 7 | 8 | # Exclude tools configuration 9 | .vscode 10 | .npmrc 11 | .npmignore 12 | .taskkey 13 | .travis.yml 14 | 15 | # Exclude generated output 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | compiler: default 3 | node_js: 4 | - '8' 5 | 6 | before_install: 7 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 8 | - sudo apt-get -yq update 9 | - sudo apt-get -yq --no-install-suggests --no-install-recommends --force-yes install g++-6 10 | 11 | install: 12 | - npm install 13 | 14 | script: 15 | - npm test -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Winery.js 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://travis-ci.org/Microsoft/wineryjs.svg?branch=master)](https://travis-ci.org/Microsoft/wineryjs) 2 | # Winery.js 3 | 4 | Winery.js is a framework that enables services to run experiments along with production traffic in the same process. Besides A/B testing, it supports experimentation at per-request level, which minimizes turnaround time when code evolves fast. Winery.js also provides a structure for creating applications declaratively, with the access to [Napa.js](https://github.com/Microsoft/napajs) capabilities, such as multi-threading, pluggable logging, metric, and etc. Before this work was branched out as an open source project, it has been used in Bing to empower feature experiments for machine learned models. 5 | 6 | ## Installation 7 | ``` 8 | npm install winery 9 | ``` 10 | 11 | ## Quick Start 12 | 13 | ```js 14 | const hub = require('winery').hub(); 15 | hub.register('winery/examples/playground', ['playground']); 16 | 17 | const request = { 18 | "application": "playground", 19 | "entryPoint": "sort", 20 | "input": [12, 16, 1, 10, 2, 5, 0], 21 | "overrideObjects": [ 22 | { 23 | "name": "comparator", 24 | "value": { 25 | "_type": "Function", 26 | "function": "function (a, b) { return a.toString() >= b.toString(); } " 27 | } 28 | } 29 | ] 30 | }; 31 | 32 | hub.serve(request) 33 | .then((response) => { 34 | console.log(response); 35 | }); 36 | 37 | ``` 38 | console output: 39 | ```js 40 | { responseCode:0, output: [0, 1, 10, 12, 16, 2, 5] } 41 | ``` 42 | 43 | ## Features 44 | - Support request level dependency injection for rapid development 45 | - Support request template level dependency injection for A/B testing 46 | - Rich dependency injection capabilities: data, functions and behaviors 47 | - Declarative application framework 48 | - Flexible flow control: Entrypoint with stacked interceptors 49 | - Integrated with Napa.js to support computation heavy scenarios 50 | - Multi-tenancy with resource sharing among multiple applications 51 | - Built-in instrumentation and monitoring support 52 | 53 | 54 | ## Overview 55 | 56 | Winery.js was built based on the idea of dependency injection at multiple levels, thus its core is to encapsulate object creation and object retrieval behaviors with an overriding mechanism. White paper [Continuous modification: a process to build constantly evolving services](https://github.com/daiyip/continuous-modification) discussed this idea in details. 57 | 58 | In Winery.js' implementation, [**Object Context**](https://github.com/Microsoft/wineryjs/tree/master/docs/api/object-context.md) serves the purpose to capture these behaviors, whose instances are owned by multiple runtime entities with different lifetime and configurability. These object context objects work collaboratively to form an overriding chain among these entities. 59 | 60 | These runtime entities are: 61 | - [**Host**](https://github.com/Microsoft/wineryjs/tree/master/docs/api/host.md): a conceptually singleton object to host applications. Live long and is configurable at deployment time. 62 | - [**Application**](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md): multi-instance object that manages resources for request execution and serve user requests. Live long and is configurable at deployment time. 63 | - [**Request Template**](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request-template.md): multi-instance object that manages different parameters and resources for A/B testing. Live long and is configurable at runtime. 64 | - [**Request**](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request.md): multi-instance object that describes request from user. Live short and is configurable at runtime. 65 | 66 | 67 | ![Winery.js Architecture](https://github.com/Microsoft/wineryjs/tree/master/docs/images/architecture.png) 68 | 69 | ## Specification 70 | - [Object Context](https://github.com/Microsoft/wineryjs/tree/master/docs/api/object-context.md) 71 | - [Overriding Rule](https://github.com/Microsoft/wineryjs/tree/master/docs/api/object-context.md#overriding-rule) 72 | - [Object Types](https://github.com/Microsoft/wineryjs/tree/master/docs/api/object-context.md#object-types) 73 | - [Object Providers](https://github.com/Microsoft/wineryjs/tree/master/docs/api/object-context.md#object-providers) 74 | - [Named Objects](https://github.com/Microsoft/wineryjs/tree/master/docs/api/object-context.md#named-objects) 75 | - [Host](https://github.com/Microsoft/wineryjs/tree/master/docs/api/host.md) 76 | - [Application Registration](https://github.com/Microsoft/wineryjs/tree/master/docs/api/host.md#application-registration) 77 | - [Request Serving](https://github.com/Microsoft/wineryjs/tree/master/docs/api/host.md#request-serving) 78 | - [Application](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md) 79 | - [Request Execution](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md#request-execution) 80 | - [Request Context](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md#request-context) 81 | - [Interceptors](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md#interceptors) 82 | - [Entry Points](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md#entry-points) 83 | - [Application-level Resources](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md#application-level-resources) 84 | - [Monitoring](https://github.com/Microsoft/wineryjs/tree/master/docs/api/application.md#monitoring) 85 | - [Request Template](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request-template.md) 86 | - [Template-level Resources](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request-template.md#template-level-resources) 87 | - [Template Inheritance](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request-template.md#template-inheritance) 88 | - [Examples](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request-template.md#examples) 89 | - [Request](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request.md) 90 | - [Basic Fields](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request.md#basic-fields) 91 | - [Override Fields](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request.md#override-fields) 92 | - [Examples](https://github.com/Microsoft/wineryjs/tree/master/docs/api/request.md#examples) 93 | - [Response](https://github.com/Microsoft/wineryjs/tree/master/docs/api/response.md) 94 | - [Basic Fields](https://github.com/Microsoft/wineryjs/tree/master/docs/api/response.md#basic-fields) 95 | - [Debug Information](https://github.com/Microsoft/wineryjs/tree/master/docs/api/response.md#debug-information) 96 | - [Performance Information](https://github.com/Microsoft/wineryjs/tree/master/docs/api/response.md#performance-information) 97 | - [Examples](https://github.com/Microsoft/wineryjs/tree/master/docs/api/response.md#examples) 98 | 99 | # Contribute 100 | You can contribute to Winery.js in following ways: 101 | 102 | * [Report issues](https://github.com/Microsoft/wineryjs/issues) and help us verify fixes as they are checked in. 103 | * Review the [source code changes](https://github.com/Microsoft/wineryjs/pulls). 104 | * Contribute bug fixes. 105 | 106 | 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. 107 | 108 | # License 109 | Copyright (c) Microsoft Corporation. All rights reserved. 110 | 111 | Licensed under the [MIT](https://github.com/Microsoft/napajs/blob/master/LICENSE.txt) License. 112 | -------------------------------------------------------------------------------- /config/builtin-entrypoints.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "listApplications", 4 | "description": "List all applications in current system.", 5 | "value": { 6 | "_type": "EntryPoint", 7 | "moduleName": "../lib/builtin-entrypoints", 8 | "functionName": "listApplications", 9 | "displayRank": 900, 10 | "exampleRequests": [ 11 | { 12 | "application": "example", 13 | "entryPoint": "listApplications" 14 | } 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "listEntryPoints", 20 | "description": "List all entry points for current application.", 21 | "value": { 22 | "_type": "EntryPoint", 23 | "moduleName": "../lib/builtin-entrypoints", 24 | "functionName": "listEntryPoints", 25 | "displayRank": 900, 26 | "exampleRequests": [ 27 | { 28 | "application": "example", 29 | "entryPoint": "listEntryPoints" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "name": "listNamedObjects", 36 | "description": "List all named objects for current application.", 37 | "value": { 38 | "_type": "EntryPoint", 39 | "moduleName": "../lib/builtin-entrypoints", 40 | "functionName": "listNamedObjects", 41 | "displayRank": 900, 42 | "exampleRequests": [ 43 | { 44 | "application": "example", 45 | "entryPoint": "listNamedObjects" 46 | } 47 | ] 48 | } 49 | }, 50 | { 51 | "name": "listTypes", 52 | "description": "List all types supported in current application.", 53 | "value": { 54 | "_type": "EntryPoint", 55 | "moduleName": "../lib/builtin-entrypoints", 56 | "functionName": "listTypes", 57 | "displayRank": 900, 58 | "exampleRequests": [ 59 | { 60 | "application": "example", 61 | "entryPoint": "listTypes" 62 | } 63 | ] 64 | } 65 | }, 66 | { 67 | "name": "listProviders", 68 | "description": "List URI providers supported in current application.", 69 | "value": { 70 | "_type": "EntryPoint", 71 | "moduleName": "../lib/builtin-entrypoints", 72 | "functionName": "listProviders", 73 | "displayRank": 900, 74 | "exampleRequests": [ 75 | { 76 | "application": "example", 77 | "entryPoint": "listProviders" 78 | } 79 | ] 80 | } 81 | }, 82 | { 83 | "name": "getNamedObject", 84 | "description": "Get an object as JSON based on current application.", 85 | "value": { 86 | "_type": "EntryPoint", 87 | "moduleName": "../lib/builtin-entrypoints", 88 | "functionName": "getNamedObject", 89 | "displayRank": 900, 90 | "exampleRequests": [ 91 | { 92 | "application": "example", 93 | "entryPoint": "getNamedObject", 94 | "input": { 95 | "name": "getType" 96 | } 97 | } 98 | ] 99 | } 100 | }, 101 | { 102 | "name": "getType", 103 | "description": "Get definition of a type in current application.", 104 | "value": { 105 | "_type": "EntryPoint", 106 | "moduleName": "../lib/builtin-entrypoints", 107 | "functionName": "getType", 108 | "displayRank": 900, 109 | "exampleRequests": [ 110 | { 111 | "application": "example", 112 | "entryPoint": "getType", 113 | "input": { 114 | "typeName": "functionName" 115 | } 116 | } 117 | ] 118 | } 119 | }, 120 | { 121 | "name": "getProvider", 122 | "description": "Get the provider definition for a URI protocol.", 123 | "value": { 124 | "_type": "EntryPoint", 125 | "moduleName": "../lib/builtin-entrypoints", 126 | "functionName": "getProvider", 127 | "displayRank": 900, 128 | "exampleRequests": [ 129 | { 130 | "application": "example", 131 | "entryPoint": "getProvider", 132 | "input": { 133 | "protocolName": "te" 134 | } 135 | } 136 | ] 137 | } 138 | } 139 | ] 140 | -------------------------------------------------------------------------------- /config/builtin-interceptors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "passThrough", 4 | "description": "Interceptor to pass through current interception. ", 5 | "value": { 6 | "_type": "Interceptor", 7 | "moduleName": "../lib/builtin-interceptors", 8 | "functionName": "passThrough" 9 | } 10 | }, 11 | { 12 | "name": "shortCircuit", 13 | "description": "Interceptor to short circuit execution. ", 14 | "value": { 15 | "_type": "Interceptor", 16 | "moduleName": "../lib/builtin-interceptors", 17 | "functionName": "shortCircuit" 18 | } 19 | }, 20 | { 21 | "name": "executeEntryPoint", 22 | "description": "Interceptor to execute entrypoint from request context. ", 23 | "value": { 24 | "_type": "Interceptor", 25 | "moduleName": "../lib/builtin-interceptors", 26 | "functionName": "executeEntryPoint" 27 | } 28 | }, 29 | { 30 | "name": "finalizeResponse", 31 | "description": "Interceptor to finalize response. ", 32 | "value": { 33 | "_type": "Interceptor", 34 | "moduleName": "../lib/builtin-interceptors", 35 | "functionName": "finalizeResponse" 36 | } 37 | }, 38 | { 39 | "name": "logRequest", 40 | "description": "Interceptor to log request. ", 41 | "value": { 42 | "_type": "Interceptor", 43 | "moduleName": "../lib/builtin-interceptors", 44 | "functionName": "logRequest" 45 | } 46 | }, 47 | { 48 | "name": "logResponse", 49 | "description": "Interceptor to log response. ", 50 | "value": { 51 | "_type": "Interceptor", 52 | "moduleName": "../lib/builtin-interceptors", 53 | "functionName": "logResponse" 54 | } 55 | }, 56 | { 57 | "name": "logRequestResponse", 58 | "description": "Interceptor to log request and response. ", 59 | "value": { 60 | "_type": "Interceptor", 61 | "moduleName": "../lib/builtin-interceptors", 62 | "functionName": "logRequestResponse" 63 | } 64 | } 65 | ] -------------------------------------------------------------------------------- /config/builtin-types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "typeName": "Function", 4 | "description": "Constructor for Function object.", 5 | "moduleName": "../lib/builtin-types", 6 | "functionName": "createFunction", 7 | "schema": "winery/schema/function-def.schema.json", 8 | "exampleObjects": [ 9 | { 10 | "_type": "Function", 11 | "function": "function() { return 0; }" 12 | }, 13 | { 14 | "_type": "Function", 15 | "moduleName": "someModule", 16 | "functionName": "someNamespace.someFunction" 17 | } 18 | ] 19 | }, 20 | { 21 | "typeName": "EntryPoint", 22 | "description": "Entrypoint type .", 23 | "moduleName": "../lib/builtin-types", 24 | "functionName": "createEntryPoint", 25 | "schema": "winery/schema/entrypoint-def.schema.json", 26 | "exampleObjects": [ 27 | { 28 | "_type": "EntryPoint", 29 | "moduleName": "someModule", 30 | "functionName": "someNamespace.someFunction", 31 | "displayRank": 100 32 | // Using default execution stack. 33 | }, 34 | { 35 | "_type": "EntryPoint", 36 | "moduleName": "someModule", 37 | "functionName": "someNamespace.someFunction", 38 | "displayRank": 100, 39 | // Using custom execution stack. 40 | "executionStack": [ 41 | "logRequestResponse", 42 | "finalizeResponse", 43 | "executeEntryPoint" 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "typeName": "Interceptor", 50 | "description": "Interceptor is a function defined as: (requestContext: RequestContext) => Promise.", 51 | "moduleName": "../lib/builtin-types", 52 | "functionName": "createInterceptor", 53 | "schema": "winery/schema/interceptor-def.schema.json", 54 | "exampleObjects": [ 55 | { 56 | "_type": "Interceptor", 57 | "moduleName": "someModule", 58 | "functionName": "someNamespace.someFunction" 59 | }, 60 | { 61 | "_type": "Interceptor", 62 | "function": "function(context) { return context.continueExecution(); } " 63 | } 64 | 65 | ] 66 | } 67 | ] -------------------------------------------------------------------------------- /config/host.json: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////// 2 | // This file contains host settings for winery. 3 | // Users can do per-application override of these values. 4 | 5 | { 6 | // Allow per-request override or not. 7 | "allowPerRequestOverride": true, 8 | 9 | // Throw exception on error or return response with error code. 10 | "throwExceptionOnError": true, 11 | 12 | // Default execution stack of interceptors if not specified per-application or per-entryPoint. 13 | "defaultExecutionStack": [ 14 | "finalizeResponse", 15 | "executeEntryPoint" 16 | ], 17 | 18 | // Default available object types. 19 | "objectTypes": [ 20 | "./builtin-types.json" 21 | ], 22 | 23 | // Default available object providers. 24 | "objectProviders": [ 25 | ], 26 | 27 | // Default available named objects. 28 | "namedObjects": [ 29 | "./builtin-interceptors.json", 30 | "./builtin-entrypoints.json" 31 | ] 32 | } -------------------------------------------------------------------------------- /docs/api/host.md: -------------------------------------------------------------------------------- 1 | # Host 2 | **Host** is an entity to host [Applications](./application.md) on various environments (Node event loop or [Napa zones](https://github.com/Microsoft/napajs/blob/master/docs/api/zone.md#intro) or mixed) 3 | 4 | To support computation intensive scenarios, host supports dispatching requests to another JavaScripts thread via [Napa zones](https://github.com/Microsoft/napajs/blob/master/docs/api/zone.md#intro). 5 | 6 | There are three types of hosts underlying to federate requests among multiple JavaScript threads: 7 | - **Leaf**: host application in current JavaScript thread. 8 | - **Proxy**: route requests to remote JavaScript thread (zone worker) 9 | - **Hub**: route request among leaf host and proxies. 10 | 11 | The diagram below depicts how different host types work together to serve requests under multiple JavaScript threads: 12 | 13 | ![](../images/hosting.png) 14 | 15 | In practice, developers can only create the hub type host regardless whether applications are registered with multiple Napa zones or just under Node event loop, it will automatically generate leaf and proxy during application registering. 16 | 17 | Following code creates a host: 18 | ```ts 19 | import * as winery from 'winery'; 20 | let host = winery.hub(); 21 | ``` 22 | 23 | Internally, Host holds a host-level [Object Context](./object-context.md) which can be overriden from upper layers. [Built-in types](../../config/builtin-types.json), [built-in interceptors](../../config/builtin-interceptors.json) and [built-in entry points](../../config/builtin-entrypoints.json) are defined here. It's not intended for users to modify host configuration at present time. 24 | 25 | Host as the facade of Winery.js runtime, provides two methods: 26 | - Register applications to run on this host 27 | - Serve requests with registered applications 28 | 29 | Its programming interface `Host` is defined in [`lib/host.ts`](../../lib/host.md) as: 30 | 31 | ```ts 32 | /// Interface for application host. 33 | export interface Host { 34 | /// Register an application instance in current host. 35 | /// full module path of a winery application. 36 | /// a list of strings used as names of application instances. 37 | /// zone to run the app. If undefined, use current isolate. 38 | register( 39 | appModulePath: string, 40 | appInstanceNames: string[], 41 | zone: napa.zone.Zone): void; 42 | 43 | /// Serve a request. 44 | /// A JSON string or a request object. 45 | serve(request: string | Request): Promise; 46 | 47 | /// Get application instance names served by this host. 48 | applicationInstanceNames: string[]; 49 | } 50 | ``` 51 | 52 | ## Application Registration 53 | A host can host multiple applications at the same time, each application can be registered with multiple instance names (aliases) via method `Host.register`. Decoupling application instance name from applicaton module name gives us the flexibility to switch modules without impacting user inputs, since instance name will be used as the key to dispatch requests to the right application, which is specified by property *"application"* from [Request](./request.md#basic-fields). 54 | 55 | 56 | Here is an examples to register multiple applications served in multiple zones. 57 | 58 | ```typescript 59 | import * as napa from 'napajs'; 60 | import * as winery from 'winery'; 61 | 62 | // By using multiple container, we can define different runtime policies. 63 | // Create container1 with default settings. 64 | let zone1 = napa.zone.create('zone1'); 65 | 66 | // Create container2 with customized settings. 67 | let zone2 = napa.zone.create('zone2', { 68 | workers: 4, 69 | maxStackSize: 1048576, // 1 MB 70 | maxOldSpaceSize: 33554432, // 32 MB 71 | maxSemiSpaceSize: 33554432, // 32 MB 72 | }); 73 | 74 | let host = winery.hub(); 75 | 76 | try { 77 | // Serve an io-intensive-app in Node.JS eventloop. 78 | host.register('io-intensive-app', ['example1']); 79 | 80 | // Serve example-app2 using name 'example2' and example-app3 using name 'example3a' in zone1. 81 | host.register('example-app2', ['example2'], zone1); 82 | host.register('example-app3', ['example3a'], zone1); 83 | 84 | // Serve example-app3 using name 'example3b' and example-app4 using name 'example4' in zone2. 85 | host.register('example-app3', ['example3b'], zone2); 86 | host.register('example-app4', ['example4'], zone2); 87 | } 88 | catch (e) { 89 | console.log("winery register failed:" + e); 90 | } 91 | 92 | ``` 93 | ## Request Serving 94 | With application registered, users can call `Host.serve` to process request and get response. 95 | ```ts 96 | 97 | import {Request, Response} from 'winery' 98 | 99 | // Create a request. 100 | let request: Request = { 101 | application: 'example1', 102 | entrypoint: 'echo', 103 | input: 'hello, world' 104 | }; 105 | 106 | // Get a response. 107 | host.serve(request) 108 | .then((response: Response) => { 109 | console.log(response); 110 | }); 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/api/object-context.md: -------------------------------------------------------------------------------- 1 | # Object Context 2 | 3 | **Object Context** is an entity to encapsulate behaviors for object creation and retrieval at runtime. 4 | 5 | Its interface `ObjectContext` is defined in [`lib/object-context.ts`](../../lib/object-context.ts) as following: 6 | 7 | ```ts 8 | /// Interface for ObjectContext 9 | export interface ObjectContext { 10 | /// Create an object from input. 11 | /// Input JS value. 12 | /// Created object or null if failed. 13 | create(input: any): any; 14 | 15 | /// Get an named object from current context. 16 | /// Name. case-sensitive. 17 | /// Named object, or undefined if not found. 18 | get(name: string): NamedObject; 19 | 20 | /// Iterate each object on current context. Overriden object will only be visited once from higher scope. 21 | /// Callback on each named object. 22 | forEach(callback: (object: NamedObject) => void): void; 23 | 24 | /// Return the base directory of this object context. 25 | /// User can use this directory to resolve files with relative paths. 26 | /// 27 | baseDir: string; 28 | } 29 | ``` 30 | ## Overriding Rule 31 | Object context can be chained to form an override relationship, that child context can inherit/override behaviors from its parent. 32 | 33 | By chaining all runtime entities together, from `Request`, `RequestTemplate` to `Application` and `Host`, users can override behaviors for object creation and retrieval from more specific level to more general level. 34 | 35 | ![Object Context Overriding Chain](../images/object-context-chain.png) 36 | 37 | ## Object Creation 38 | Winery.js provides two ways for object creation, one is to construct an object from a plain JavaScript object input through **object types**, the other is to create an object from a URI through **object providers** 39 | 40 | ### Object Types 41 | Object types are mapped to their factory method 42 | - For primitive types, method `ObjectContext.create` simply pass through their values. 43 | 44 | - For object-type input, a string property *"_type"* is required, which will be used as a key to look up constructor (or factory method) for creating the object. 45 | 46 | An example input may look like: 47 | ```ts 48 | const input = { 49 | _type: "Point", 50 | x: 1, 51 | y: 2 52 | } 53 | ``` 54 | #### Factory Method 55 | The object definition and factory method may be defined as: 56 | 57 | ```ts 58 | export class Point2D { 59 | public x: number; 60 | public y: number; 61 | constructor(x: number, y: number) { 62 | this.x = x; 63 | this.y = y; 64 | } 65 | 66 | public distance(pt: Point2D): number { 67 | return Math.sqrt( 68 | Math.pow(this.x - pt.x) 69 | + Math.pow(this.y - pt.y)); 70 | } 71 | } 72 | 73 | // Factory method (or constructor) function for Point2D 74 | export function createPoint(input: any): Point2D { 75 | return new Point(input.x, input.y); 76 | } 77 | ``` 78 | #### Registration 79 | To associate *Point* from property `"_type"` with constructor `createPoint`, a type registration is required in JSON (see [schema](../../schema/object-type-config.schema.json) or interface [TypeDef](../../lib/object-type.ts)). 80 | 81 | It can be configured at `Host` and `Application` level by JSON files referenced in property *objectTypes* in [Host Config]() and [Application Config](). or get overriden at `RequestTemplate` and `Request` level by elements under property *overrideTypes*. 82 | 83 | The registration entry for *Point* shall look like: 84 | ```json 85 | { 86 | "typeName": "Point", 87 | "description": "Constructor for Point2D.", 88 | "moduleName": "point2d", 89 | "functionName": "createPoint", 90 | "exampleObjects": [ 91 | { 92 | "_type": "Point", 93 | "x": 0, 94 | "y": 1 95 | } 96 | ] 97 | } 98 | 99 | ``` 100 | You can also check out [registration for built-in types](../../config/builtin-types.json). 101 | 102 | #### Creating objects with types 103 | Following code creates two `Point2D` objects at runtime and print their distance: 104 | ```ts 105 | import {ObjectContext} from 'winery' 106 | 107 | function doSomething(context: ObjectContext): void { 108 | const pt1: Point2D = context.create({ _type: "Point", x: 0, y: 0}); 109 | const pt2: Point2D = context.create({ _type: "Point", x: 1, y: 1}); 110 | console.log(pt1.distance(pt2)); 111 | } 112 | ``` 113 | Please note that you should always use `ObjectContext` to create the objects instead of `new` it directly if you want to override the object creation behavior later. 114 | 115 | ### Object Providers 116 | In many cases, it's beneficial to describe an object using an URI, like "file://abc.txt", or "http://www.something.com/a", and etc. Therefore, Winery.js also supports creating an object from an [URI](../../lib/object-provider.ts). 117 | 118 | It's very similar to creating objects from plain objects, except that the input is just a string in the format of an URI. 119 | 120 | #### Provider Method 121 | For example, if we are going to support creation of `Point2D` from URI: *"pt:/?x=1&y=2"*, we can do: 122 | ```ts 123 | import {Uri} from "winery"; 124 | 125 | // Provider function for Point2D. 126 | export createPoint2DFromURI(uri: Uri) { 127 | const x = parseFloat(uri.getParameter('x')); 128 | const y = parseFloat(uri.getParameter('y')); 129 | return new Point2D(x, y); 130 | } 131 | ``` 132 | #### Registration 133 | Following JSON object (see [schema](../../schema/object-provider-config.schema.json) or interface [ProviderDef](../../lib/object-provider.ts)) registers `createPoint2DFromURI` as the provider function for protocol *"pt"*: 134 | ```json 135 | { 136 | "protocol": "pt", 137 | "description": "Point2D", 138 | "moduleName": "point2d", 139 | "functionName": "createPoint2DFromURI", 140 | "exampleUri": [ 141 | "pt:/?x=0&y=0" 142 | ] 143 | } 144 | ``` 145 | Similarly, provider methods can be registered at `Host` and `Application` level in files referenced by property *"objectProviders"*, or get overriden at `RequestTemplate` and `Request` level in elements of property *"overrideProviders"*. 146 | 147 | #### Using Object Providers 148 | To create Point2D from URIs at runtime: 149 | ```ts 150 | function doSomething(context: ObjectContext): void { 151 | const pt1: Point2D = context.create("pt:/?x=0&y=0"); 152 | const pt2: Point2D = context.create("pt:/?x=1&y=1"); 153 | console.log(pt1.distance(pt2)); 154 | } 155 | ``` 156 | 157 | ## Object Retrieval 158 | How to retrive an existing object is another dimension of the problem. Winery.js supports this functionality via **Named Object**, which is an object associated with a global unique name. 159 | 160 | ### Named Objects 161 | Named objects are defined by interface `NamedObject`, which is created from `NamedObjectDef`. `NamedObjectDef` can be specified from a plain JavaScript object or from JSON. 162 | 163 | Both of them are defined in [`./lib/named-object.ts`](../../lib/named-object.ts): 164 | ```ts 165 | export interface NamedObjectDef { 166 | /// Name or key to retrieve this object. 167 | name: string; 168 | 169 | /// Description for this object. 170 | description?: string; 171 | 172 | /// If this object is private, means that cannot be listed by entry points `listNamedObjects`. 173 | private?: boolean; 174 | 175 | /// If this object overrides a previous object definition with the same name. 176 | /// This may be useful if you borrow definition file from other apps and want to override individual ones. 177 | /// 178 | override?: boolean; 179 | 180 | /// Value of the input to create this named object, which is described by plain JavaScript object or URI. 181 | /// The plain JavaScript object / URI can be constructed by registered ObjectFactory and ObjectProvider. 182 | /// 183 | value: any; 184 | 185 | /// Dependency from current definition to object context. This is calculated automatically. 186 | dependencies?: ObjectContextDependency; 187 | } 188 | 189 | export interface NamedObject { 190 | /// Definition of current named object. 191 | def: NamedObjectDef; 192 | 193 | /// Value of current named object 194 | value: any; 195 | 196 | /// Scope of where this named object is provided. 197 | readonly scope: string; 198 | } 199 | ``` 200 | ### Registration 201 | A JSON object below conforms to the [schema](../../schema/named-object-config.schema.json) of `NamedObjectDef`, which registers a `Point2D` object with name *"CenterOfUniverse"*: 202 | ```json 203 | { 204 | "name": "CenterOfUniverse", 205 | "value": { 206 | "_type": "Point", 207 | "x": 0, 208 | "y": 1 209 | } 210 | } 211 | ``` 212 | The same as [`ObjectTypes`](#from-plain-javascript-object) and [`ObjectProviders`](#from-uri), `NamedObject` can be registered at `Host` and `Application` level by files under property *"namedObjects"*, or get overriden at `RequestTemplate` and `Request` level by carrying the new definition under property *"overrideObjects"*. 213 | 214 | 215 | ### Using Named Objects 216 | Following code demostrates the retrival of this pre-defined object at runtime. 217 | ```ts 218 | function doSomething(context: ObjectContext): void { 219 | const center: Point2D = context.get("CenterOfUniverse").value; 220 | 221 | console.log(center); 222 | } 223 | ``` 224 | -------------------------------------------------------------------------------- /docs/api/request-template.md: -------------------------------------------------------------------------------- 1 | # Request Template 2 | 3 | **Request Template** is an object that pre-configure resources for serving requests. Multiple request templates can be loaded in memory on the same host, thus they can be used for serving the same requests with different server-side treatment. One common usage is for A/B testing. Objects declared in request template are loaded when request template is loaded, thus there is no performance impact during request serving time. 4 | 5 | A request template must have a parent context, it's either an [Application](./application.md) specified by property *"application"* or a base template specified by property *"base"*. 6 | ## Template-level Resources 7 | Users can define [Object Context](./object-context.md) at template level, which overrides object context at application level and host level. 8 | 9 | The elements to define template-level object context are the same as those in [Request](./request.md). They are: 10 | 11 | | Property name | Required | Description | 12 | |-------------------|----------|--------------------------------------------------------------------------------------------------------------------------| 13 | | overrideTypes | N | override constructor of objects created [from plain JavaScript object](./object-context.md#from-plain-javascript-object) | 14 | | overrideProviders | N | override provider of objects created [from URI](./object-context.md#from-uri) | 15 | | overrideObjects | N | override [named objects](./object-context.md#named-object) | 16 | 17 | 18 | ## Template Inheritance 19 | A request template can extend another request template, which inherits the Object Context from its base, and can override with its own (see [Overriding Rule](./object-context.md#overriding-rule)). Request object can be thought as a finalized request template during request time, which comes with an overhead of creating its resources per request. 20 | 21 | Template inheritance enables a structured development process to manage changes layer by layer incrementally, as well as sharing resources among multiple templates. 22 | 23 | ![Templates organized in layers](../images/request-template.png) 24 | 25 | ## Examples 26 | 27 | ### A simple baseless template 28 | Following JSON file defines a baseless request template that applies to application *"example"* with overriden object types, providers and named objects. 29 | ```json 30 | { 31 | "application": "example", 32 | "overrideTypes": [ 33 | { 34 | "typeName": "Type1", 35 | "moduleName": "module1", 36 | "functionName": "function1" 37 | } 38 | ], 39 | "overrideProviders": [ 40 | { 41 | "protocol": "protocol1", 42 | "moduleName": "module1", 43 | "functionName": "function2" 44 | } 45 | ], 46 | "overrideObjects": [ 47 | { 48 | "name": "object1", 49 | "value": { 50 | "someProp": "someValue" 51 | } 52 | } 53 | ] 54 | } 55 | ``` 56 | ### A template with a base 57 | Following JSON file defines a request template extending a base template, with overriden object types, providers and named objects. 58 | ```json 59 | { 60 | "base": "./base-template.json", 61 | "overrideTypes": [ 62 | { 63 | "typeName": "Type1", 64 | "moduleName": "module1", 65 | "functionName": "function1" 66 | } 67 | ], 68 | "overrideProviders": [ 69 | { 70 | "protocol": "protocol1", 71 | "moduleName": "module1", 72 | "functionName": "function2" 73 | } 74 | ], 75 | "overrideObjects": [ 76 | { 77 | "name": "object1", 78 | "value": { 79 | "someProp": "someValue" 80 | } 81 | } 82 | ] 83 | } 84 | ``` -------------------------------------------------------------------------------- /docs/api/request.md: -------------------------------------------------------------------------------- 1 | # Request 2 | 3 | `Request` is a plain JavaScript object to describe requests sent to an `Application`. 4 | 5 | It's defined by [JSON schema](../../schema/request.schema.json) or interface [`Request`](../../lib/request.ts): 6 | 7 | ```ts 8 | /// Interface for control flags. 9 | export type ControlFlags = { 10 | /// Enable debugging or not. Set to false by default. 11 | debug?: boolean; 12 | 13 | /// Return performance numbers or not. Set to false by default. 14 | perf?: boolean; 15 | } 16 | 17 | /// Interface for winery request. 18 | export interface Request { 19 | /// Registered application instance name. Required unless "base" is present. 20 | application?: string; 21 | 22 | /// Uri for request template to apply. Optional. 23 | base?: string 24 | 25 | /// Entry point name 26 | entryPoint: string; 27 | 28 | /// Trace ID 29 | traceId?: string; 30 | 31 | /// User input as the 1st argument passing to entry point function 32 | input?: any; 33 | 34 | /// Control flags 35 | controlFlags?: ControlFlags; 36 | 37 | /// Overridden types 38 | overrideTypes?: TypeDef[]; 39 | 40 | /// Overridden named objects 41 | overrideObjects?: NamedObjectDef[]; 42 | 43 | /// Overridden providers 44 | overrideProviders?: ProviderDef[]; 45 | } 46 | ``` 47 | There are two groups of fields in `Request`: 48 | - Basic fields that describes request destination and input of service method 49 | - Fields that define request level ObjectContext to override request template level or application level ObjectContext. 50 | 51 | ![Request Object Layout](../images/request-layout.png) 52 | ## Basic Fields 53 | | Property name | Required | Description | 54 | |---------------|------------------------------|---------------------------------------------------------------------------------------------------------| 55 | | application | Y unless `base` is specified | Application instance name (or alias) to request for service, it's required unless `base` is specified. | 56 | | base | N | Path of base template that applies to this request. Mutual-exclusive with `application` | 57 | | entrypoint | Y | Name of the entrypoint object to request for service | 58 | | input | N | User object as input that will be passed to entrypoint function, whose schema is entrypoint specific | 59 | | controlFlags | N | Flags for enabling debugging and instrumentation | 60 | 61 | ## Override Fields 62 | Further, object creation and object retrieval behaviors can be overriden from request with properties with prefix *"override"*. 63 | 64 | | Property name | Required | Description | 65 | |-------------------|----------|--------------------------------------------------------------------------------------------------------------------------| 66 | | overrideTypes | N | override constructor of objects created [from plain JavaScript object](./object-context.md#from-plain-javascript-object) | 67 | | overrideProviders | N | override provider of objects created [from URI](./object-context.md#from-uri) | 68 | | overrideObjects | N | override [named objects](./object-context.md#named-object) | 69 | 70 | 71 | ## Examples 72 | 73 | Example 1: a simple request with only application / entryPoint: 74 | ```ts 75 | { 76 | application: "example", 77 | entryPoint: "keepAlive" 78 | } 79 | ``` 80 | 81 | Example 2: a request using template, which inherits objects context from the template: 82 | ```ts 83 | { 84 | base: "example/plan1.json", 85 | entryPoint: "doSomething" 86 | } 87 | ``` 88 | 89 | Example 3: a request for summing a list of numbers: 90 | ```ts 91 | { 92 | application: "math", 93 | entryPoint: "sum", 94 | input: [1, 2] 95 | } 96 | ``` 97 | 98 | Example 4: a request that override an entry point: 99 | ```ts 100 | { 101 | application: "example", 102 | entryPoint: "echo", 103 | input: "hello", 104 | overrideObjects: [ 105 | { 106 | "name": "echo", 107 | "value": { 108 | "_type": "EntryPoint", 109 | "function": "function(input) { return input + ', winery'; }" 110 | } 111 | } 112 | ] 113 | } 114 | ``` 115 | 116 | Example 5: a request that override an object type: 117 | ```ts 118 | { 119 | application: "example", 120 | entryPoint: "echo", 121 | input: "hello", 122 | overrideTypes: [ 123 | { 124 | "typeName": "Document", 125 | "moduleName": "some-doc", 126 | "functionName": "create" 127 | } 128 | ] 129 | } 130 | ``` 131 | 132 | 133 | Example 6: a request that override an object provider: 134 | ```ts 135 | { 136 | application: "example", 137 | entryPoint: "echo", 138 | input: "hello", 139 | overrideProviders: [ 140 | { 141 | "protocol": "ftp", 142 | "moduleName": "ftp-client", 143 | "functionName": "get" 144 | } 145 | ] 146 | } 147 | ``` 148 | 149 | 150 | Example 7: a request using a template overrides a few types, provider and objects for only this request: 151 | ```ts 152 | { 153 | base: "example/plan2.json", 154 | entryPoint: "print", 155 | input: { 156 | "document": { 157 | "_type": "Document", 158 | "value": { 159 | "source": "ftp://wineryjs.org/a.txt" 160 | } 161 | } 162 | }, 163 | overrideTypes: [ 164 | { 165 | "typeName": "Document", 166 | "moduleName": "some-doc", 167 | "functionName": "create" 168 | } 169 | ], 170 | overrideProviders: [ 171 | { 172 | "protocol": "ftp", 173 | "moduleName": "ftp-client", 174 | "functionName": "get" 175 | } 176 | ], 177 | overrideObjects: [ 178 | { 179 | "name": "printer", 180 | "value": "printer://printer-at-a-different-address" 181 | } 182 | ] 183 | } 184 | ``` 185 | 186 | Example 8: a request asking for debug information and performance information. 187 | ```ts 188 | { 189 | application: "example", 190 | entryPoint: "doSomething", 191 | controlFlags: { 192 | debug: true, 193 | perf: true 194 | } 195 | } 196 | ``` 197 | 198 | -------------------------------------------------------------------------------- /docs/api/response.md: -------------------------------------------------------------------------------- 1 | # Response 2 | 3 | `Response` is a plain JavaScript object to describe responses returned from an `Application`. 4 | 5 | It's defined by [JSON schema](../../schema/response.schema.json) or interface [`Response`](../../lib/response.ts) as following: 6 | 7 | ```ts 8 | // Response code 9 | export enum ResponseCode { 10 | // Success. 11 | Success = 0, 12 | 13 | // Internal error. 14 | InternalError = 1, 15 | 16 | // Server side timeout. 17 | ProcessTimeout = 2, 18 | 19 | // Throttled due to policy. 20 | Throttled = 3, 21 | 22 | // Error caused by bad input. 23 | InputError = 4 24 | } 25 | 26 | /// Interface for response 27 | export interface Response { 28 | /// Response code 29 | responseCode: ResponseCode; 30 | 31 | /// Error message if response code is not Success. 32 | errorMessage?: string; 33 | 34 | /// Output from entrypoint. 35 | output?: any; 36 | 37 | /// Debug information. 38 | debugInfo?: DebugInfo; 39 | 40 | /// Performance numbers. 41 | perfInfo?: PerfInfo; 42 | } 43 | ``` 44 | ## Basic Fields 45 | 46 | | Property Name | Present | Description | 47 | |---------------|-----------------------------------------------|---------------------------------------------------| 48 | | responseCode | Always | Response code | 49 | | errorMessage | When responseCode is not 0 (success) | A brief message on why request had failed | 50 | | output | When entrypoint function has a return value | Entrypoint return value | 51 | | debugInfo | When `controlFlags.debug` set to `true` | Exception details, event logs for current request | 52 | | perfInfo | When `controlFlags.perf` set to `true` | Updated metrics for current request | 53 | 54 | ## Debug Information 55 | When `controlFlags.debug` is set to `true`, `debugInfo` is returned containing 3 optional fields: 56 | 57 | | Property name | Present | Description | 58 | |---------------|--------------------------|--------------------------------------------| 59 | | exception | When exception is thrown | Exception details | 60 | | events | Always | Output from `context.debug` sorted by time | 61 | | details | Always | Key/value pairs from `context.detail` | 62 | ### Exception 63 | Property *"exception"* is an object with following fields: 64 | 65 | | Property name | Type | Description | 66 | |---------------|--------|----------------------------------------------| 67 | | stack | string | stack trace | 68 | | message | string | exception message | 69 | | fileName | string | file name from where exception is thrown | 70 | | lineNumber | number | line number from where exception is thrown | 71 | | columnNumber | number | column number from where exception is thrown | 72 | ### Events 73 | Property *\"events\"* is an array of objects with following fields: 74 | 75 | | Property name | Type | Description | 76 | |---------------|--------|-----------------------------------------| 77 | | eventTime | Date | time when event is logged | 78 | | logLevel | string | "debug", "info", "warning", or "error" | 79 | | message | string | message of the event | 80 | 81 | ## Performance Information 82 | When `controlFlags.perf` is set to `true`, `perfInfo` will be filled. It's a dictionary of key/values, the keys are the display name of metrics, and the values are the value of these metrics. 83 | 84 | ## Examples: 85 | 86 | Example 1: a succeeded response without a return value. 87 | ```ts 88 | { 89 | responseCode: 0 90 | } 91 | ``` 92 | 93 | Example 2: a succeeded response with a string value returned from its entry point. 94 | 95 | ```ts 96 | { 97 | responseCode: 0, 98 | output: "hello winery" 99 | } 100 | ``` 101 | 102 | Example 3: a failed response due to entry point cannot find a module `abc` via `require`. 103 | 104 | ```ts 105 | { 106 | responseCode: 1, 107 | errorMessage: "Error: Cannot find module 'abc'" 108 | } 109 | ``` 110 | 111 | Example 4: a succeeded response with debug on and perf on. 112 | ```ts 113 | { 114 | responseCode: 0, 115 | debugInfo: { 116 | events: [ 117 | { 118 | eventTime: "2017-12-16T00:02:12:596Z", 119 | logLevel: "Info", 120 | message: "Request started." 121 | }, 122 | { 123 | eventTime: "2017-12-16T00:02:12:597Z", 124 | logLevel: "Info", 125 | message: "Request completed." 126 | } 127 | ], 128 | detail: { 129 | "user-debug-key1": 123, 130 | "user-debug-key2": "abc" 131 | } 132 | }, 133 | perfInfo: { 134 | "processTime[exampleApp, doSomething]": 1 135 | } 136 | } 137 | ``` 138 | 139 | Example 5: a failed response with debug on. 140 | ```ts 141 | { 142 | responseCode: 1, 143 | errorMessage: "Error: Cannot find module 'abc'", 144 | debugInfo: { 145 | exception: { 146 | stack: "at Function.Module._resolveFilename (module.js:485:15) at Function.Module._load (module.js:437:25) at Module.require (module.js:513:17) at require (internal/module.js:11:18) at repl:1:1 at ContextifyScript.Script.runInThisContext (vm.js:44:33) at REPLServer.defaultEval (repl.js:239:29) at bound (domain.js:301:14) at REPLServer.runBound [as eval] (domain.js:314:12) at REPLServer.onLine (repl.js:433:10)", 147 | message: "Error: Cannot find module 'abc'", 148 | fileName: "/home/test.js", 149 | lineNumber: 123, 150 | columnNumber: 12 151 | } 152 | } 153 | } 154 | ``` 155 | -------------------------------------------------------------------------------- /docs/images/application-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/wineryjs/f065b465cbd440b9d27a8e9bc3fef1bda8a5202d/docs/images/application-layout.png -------------------------------------------------------------------------------- /docs/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/wineryjs/f065b465cbd440b9d27a8e9bc3fef1bda8a5202d/docs/images/architecture.png -------------------------------------------------------------------------------- /docs/images/execution-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/wineryjs/f065b465cbd440b9d27a8e9bc3fef1bda8a5202d/docs/images/execution-stack.png -------------------------------------------------------------------------------- /docs/images/hosting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/wineryjs/f065b465cbd440b9d27a8e9bc3fef1bda8a5202d/docs/images/hosting.png -------------------------------------------------------------------------------- /docs/images/object-context-chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/wineryjs/f065b465cbd440b9d27a8e9bc3fef1bda8a5202d/docs/images/object-context-chain.png -------------------------------------------------------------------------------- /docs/images/request-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/wineryjs/f065b465cbd440b9d27a8e9bc3fef1bda8a5202d/docs/images/request-layout.png -------------------------------------------------------------------------------- /docs/images/request-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/wineryjs/f065b465cbd440b9d27a8e9bc3fef1bda8a5202d/docs/images/request-template.png -------------------------------------------------------------------------------- /docs/tutorial/step-by-step.md: -------------------------------------------------------------------------------- 1 | # Writing Applications 2 | 3 | ## Step 1: Coding the logics 4 | `example-app/example.ts` 5 | ```typescript 6 | import w = require('winery'); 7 | 8 | //////////////////////////////////////////////////////////////////////////// 9 | /// Functions for entrypoints. 10 | 11 | /// Function for entrypoint 'echo'. 12 | /// See 'named-objects.json' below on how we register this entrypoint. 13 | /// The 1st parameter is the input from request. 14 | /// The 2nd parameter is a winery.RequestContext object. 15 | export function echo(context: w.RequestContext, text: string) { 16 | return text; 17 | } 18 | 19 | /// Function for entrypoint 'compute', which is to compute sum on an array of numbers. 20 | export function compute(context: w.RequestContext, numberArray: number[]) { 21 | var func = (list: number[]) => { 22 | return list.reduce((sum: number, value: number) => { 23 | return sum + value; 24 | }, 0); 25 | } 26 | // Note: context.get will returned named object giving a name. 27 | var functionObject = context.get('customFunction'); 28 | if (functionObject != null) { 29 | func = functionObject.value; 30 | } 31 | return func(numberArray); 32 | } 33 | 34 | /// Function for entrypoint 'loadObject', which return an object for the uri. 35 | /// NOTE: We use URI to represent object that is able to reference and share more conveniently. 36 | export function loadObject(uri: string, context: winery.RequestContext) { 37 | // Note: context.create will detect uri string and use registered object provider to create the object. 38 | return context.create(uri); 39 | } 40 | 41 | /// Function that will be used to provide objects for protocol 'text'. 42 | export function createObject(input: any, context: winery.RequestContext) { 43 | // Note: for non-uri input, context.create will use constructor of registered object types to create it. 44 | return context.create(input); 45 | } 46 | 47 | //////////////////////////////////////////////////////////////////////////// 48 | /// Functions for object types. 49 | TODO: 50 | 51 | //////////////////////////////////////////////////////////////////////////// 52 | /// Functions for object providers. 53 | TODO: 54 | 55 | ``` 56 | ## Step 2: Configuring things together 57 | `example-app/app.json` (root configuration) 58 | ```json 59 | { 60 | "id": "example-app", 61 | "description": "Example application for winery", 62 | "objectTypes": ["./object-types.json"], 63 | "objectProviders": ["./object-providers.json"], 64 | "namedObjects": ["./named-objects.json"], 65 | "interceptors": ["./interceptors.json"], 66 | "metrics": { 67 | "sectionName": "ExampleApp", 68 | "definitions": ["./metrics.json"] 69 | } 70 | } 71 | ``` 72 | `example-app/object-types.json` (a configuration file for objectTypes) 73 | 74 | See [[Object Type]](#object-type). 75 | 76 | ```json 77 | [ 78 | { 79 | "typeName": "", 80 | "description": "", 81 | "moduleName": "", 82 | "functionName": "", 83 | "schema": "" 84 | } 85 | ] 86 | 87 | ``` 88 | 89 | `example-app/object-providers.json` (a configuration file for objectProviders) 90 | 91 | See [[Object Provider]](#object-provider) 92 | ```json 93 | [ 94 | { 95 | "protocol": "", 96 | "description": "", 97 | "moduleName": "", 98 | "functionName": "" 99 | } 100 | ] 101 | 102 | ``` 103 | `example-app/named-objects.json` (a configuration file for namedObjects) 104 | 105 | See [[Named Object]](#named-object) 106 | ```json 107 | [ 108 | { 109 | "name": "", 110 | // Object value can be created by object factory or providers. 111 | "value": {} 112 | } 113 | ] 114 | 115 | ``` 116 | ## Step 3 - Trying requests 117 | ``` 118 | TODO: 119 | ``` -------------------------------------------------------------------------------- /examples/playground/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "winery-playground", 3 | 4 | "description": "Winery.js example application - playground", 5 | 6 | "allowPerRequestOverride": true, 7 | "objectTypes": [], 8 | "objectProviders": [], 9 | "namedObjects": [ 10 | "./entrypoints.json", 11 | "./objects.json" 12 | ] 13 | } -------------------------------------------------------------------------------- /examples/playground/app.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from '../../lib/request-context'; 2 | 3 | export namespace entrypoints { 4 | /// Entrypoint: sort 5 | export function sort(context: RequestContext, input: number[]): number[] { 6 | let comparator = context.get("comparator"); 7 | return input.sort(comparator); 8 | } 9 | } 10 | 11 | /// Default comparator 12 | export function defaultComparator(a: number, b: number): number { 13 | return a - b; 14 | } -------------------------------------------------------------------------------- /examples/playground/entrypoints.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "sort", 4 | "value": { 5 | "_type": "EntryPoint", 6 | "moduleName": "./app", 7 | "functionName": "entrypoints.sort" 8 | } 9 | } 10 | ] -------------------------------------------------------------------------------- /examples/playground/objects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "comparator", 4 | "value": { 5 | "_type": "Function", 6 | "moduleName": "./app", 7 | "functionName": "defaultComparator" 8 | } 9 | } 10 | ] -------------------------------------------------------------------------------- /examples/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "experimentalDecorators": true, 6 | "noImplicitAny": true, 7 | "declaration": false, 8 | "sourceMap": false, 9 | "lib": ["es2015"], 10 | "baseUrl": "./", 11 | "paths": { 12 | "winery": ["../../"] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /lib/builtin-entrypoints.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////// 5 | // Built-in entry points supported in Winery.js 6 | 7 | import * as fs from 'fs'; 8 | import { hub, RequestContext, TypeDef, ProviderDef, NamedObjectDef } from './index'; 9 | 10 | /// List all application names in current system. 11 | export function listApplications(request: RequestContext): string[] { 12 | return hub().applicationInstanceNames; 13 | } 14 | 15 | const DEFAULT_RANK_USER_ENTRYPOINT: number = 700; 16 | 17 | /// List all entry point names under current application. 18 | export function listEntryPoints( 19 | request: RequestContext, 20 | input: { detail: boolean, allowGlobal: boolean, allowPrivate: boolean } 21 | = { detail: false, allowGlobal: true, allowPrivate: false } 22 | ): string[] | NamedObjectDef[] { 23 | 24 | let entryPointDefs: NamedObjectDef[] = []; 25 | request.app.objectContext.forEach(namedObject => { 26 | let def = namedObject.def; 27 | if (def.value._type === 'EntryPoint' 28 | && (input.allowPrivate || !namedObject.def.private) 29 | && (input.allowGlobal || namedObject.scope !== "global")) { 30 | entryPointDefs.push(namedObject.def); 31 | } 32 | }); 33 | 34 | // Rank entrypoint by displayRank first and then alphabetical order. 35 | entryPointDefs = entryPointDefs.sort((a, b): number => { 36 | let rankA = isNaN(a.value['displayRank']) ? DEFAULT_RANK_USER_ENTRYPOINT : a.value['displayRank']; 37 | let rankB = isNaN(b.value['displayRank']) ? DEFAULT_RANK_USER_ENTRYPOINT : b.value['displayRank']; 38 | 39 | if (rankA != rankB) { 40 | return rankA - rankB; 41 | } 42 | 43 | // Name should never be equal. 44 | return a.name < b.name ? -1 : 1; 45 | }); 46 | return input.detail ? entryPointDefs 47 | : entryPointDefs.map((def) => { 48 | return def.name 49 | }); 50 | } 51 | 52 | /// List all named objects under current application. 53 | export function listNamedObjects( 54 | request: RequestContext, 55 | input: { allowPrivate: boolean, scopes: string[] } = { allowPrivate: false, scopes: ['request', './application']}): string[] { 56 | 57 | let objectNames: string[] = []; 58 | request.app.objectContext.forEach(namedObject => { 59 | if ((input.allowPrivate || !namedObject.def.private) 60 | && (namedObject.scope in input.scopes)) { 61 | objectNames.push(namedObject.def.name); 62 | } 63 | }); 64 | return objectNames; 65 | } 66 | 67 | /// Display a named object by name. 68 | export function getNamedObject(request: RequestContext, input: { name: string }): any { 69 | if (input == null || input.name == null) { 70 | throw new Error("'name' property must be specified under 'input' object of request."); 71 | } 72 | 73 | let object = request.getNamedObject(input.name); 74 | if (object == null || object.def == null) { 75 | return null; 76 | } 77 | 78 | return object.def; 79 | } 80 | 81 | /// List all types supported in current application. 82 | /// TODO: @dapeng, return types from global and request scope. 83 | export function listTypes(request: RequestContext): string[] { 84 | let appDef = request.app.settings; 85 | let typeNames: string[] = []; 86 | for (let typeDef of appDef.objectContextDef.typeDefs) { 87 | typeNames.push(typeDef.typeName); 88 | } 89 | return typeNames; 90 | } 91 | 92 | /// Get definition of a type in current application. 93 | /// TODO: @dapeng, return types from global and request scope. 94 | export function getType(request: RequestContext, input: { typeName: string }): TypeDef { 95 | if (input == null || input.typeName == null) { 96 | throw new Error("'typeName' property must be specified under 'input' object of request."); 97 | } 98 | 99 | let appDef = request.app.settings; 100 | let types = appDef.objectContextDef.typeDefs; 101 | for (let i = 0; i < types.length; i++){ 102 | if (types[i].typeName.toLowerCase() == input.typeName.toLowerCase()) { 103 | return types[i]; 104 | } 105 | } 106 | 107 | throw new Error("Type name '" + input.typeName + "' is not supported in current application."); 108 | } 109 | 110 | /// List URI providers supported in current application. 111 | /// TODO: @dapeng, return providers from global and request scope. 112 | export function listProviders(request: RequestContext): string[] { 113 | let appDef = request.app.settings; 114 | let protocolNames: string[] = []; 115 | for (let providerDef of appDef.objectContextDef.providerDefs) { 116 | protocolNames.push(providerDef.protocol); 117 | } 118 | return protocolNames; 119 | } 120 | 121 | /// Get the provider definition for a URI protocol. 122 | /// TODO: @dapeng, return providers from global and request scope. 123 | export function getProvider(request: RequestContext, input: { protocolName: string }): ProviderDef { 124 | if (input == null || input.protocolName == null) { 125 | throw new Error("'protocolName' property must be specified under 'input' object of request."); 126 | } 127 | 128 | let appDef = request.app.settings; 129 | let providers = appDef.objectContextDef.providerDefs; 130 | for (let provider of providers) { 131 | if (provider.protocol.toLowerCase() === input.protocolName.toLowerCase()) { 132 | return provider; 133 | } 134 | } 135 | throw new Error("Protocol name '" + input.protocolName + "' is not supported in current application."); 136 | } 137 | -------------------------------------------------------------------------------- /lib/builtin-interceptors.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////////////////////////// 5 | /// Built-in interceptors supported in Winery.js 6 | 7 | import { log } from 'napajs'; 8 | import * as utils from './utils'; 9 | 10 | import { ResponseCode, Response } from './response' 11 | import { RequestContext } from './request-context' 12 | 13 | /// Interceptor: pass through. 14 | /// This interceptor is used for debug purpose when doing per-request override 15 | /// 16 | export async function passThrough( 17 | context: RequestContext): Promise { 18 | return await context.continueExecution(); 19 | } 20 | 21 | /// Interceptor: short circuit. 22 | /// This interceptor is used for debug purpose when doing per-request override 23 | /// 24 | export async function shortCircuit( 25 | context: RequestContext): Promise { 26 | return Promise.resolve({ 27 | responseCode: ResponseCode.Success 28 | }); 29 | } 30 | 31 | /// Interceptor: execute entryPoint 32 | export async function executeEntryPoint( 33 | context: RequestContext): Promise { 34 | 35 | let response = await context.continueExecution(); 36 | response.output = await utils.makePromiseIfNotAlready( 37 | context.entryPoint(context, context.input)); 38 | 39 | return response; 40 | } 41 | 42 | /// Interceptor: log request only. 43 | export async function logRequest( 44 | context: RequestContext): Promise { 45 | 46 | log.debug(JSON.stringify(context.request)); 47 | return await context.continueExecution(); 48 | } 49 | 50 | /// Interceptor: log response only. 51 | export async function logResponse( 52 | context: RequestContext): Promise { 53 | 54 | let response = await context.continueExecution(); 55 | log.debug(JSON.stringify(response)); 56 | return response; 57 | } 58 | 59 | /// Interceptor: log request and response. 60 | export async function logRequestResponse( 61 | context: RequestContext): Promise { 62 | 63 | log.debug(JSON.stringify(context.request)); 64 | let response = await context.continueExecution(); 65 | log.debug(JSON.stringify(response)); 66 | return response; 67 | } 68 | 69 | /// Interceptor: finalize response 70 | export async function finalizeResponse( 71 | context: RequestContext): Promise { 72 | 73 | let response = await context.continueExecution(); 74 | 75 | // Attach debug info if needed. 76 | if (context.controlFlags.debug) { 77 | response.debugInfo = context.debugger.getOutput(); 78 | } 79 | 80 | // Attach performance data if needed. 81 | if (context.controlFlags.perf) { 82 | response.perfInfo = context.perf.data; 83 | } 84 | 85 | return response; 86 | } 87 | -------------------------------------------------------------------------------- /lib/builtin-types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////////////////////////// 5 | /// Built-in object types in Winery.js 6 | 7 | import { EntryPoint, Interceptor } from './application'; 8 | import { ObjectContext } from './object-context'; 9 | import { RequestContext } from './request-context'; 10 | import { Request } from './request'; 11 | import { Response } from './response'; 12 | 13 | import * as utils from './utils'; 14 | import * as path from 'path'; 15 | 16 | /// Definition for function object. 17 | export interface FunctionDefinition { 18 | // For referencing existing function. 19 | moduleName?: string, 20 | functionName?: string, 21 | 22 | /// for inline function. 23 | function?: string; 24 | } 25 | 26 | /// Entrypoint definition. 27 | export interface EntryPointDefinition extends FunctionDefinition { 28 | /// _type === 'EntryPoint' 29 | _type: "EntryPoint", 30 | 31 | /// Optional. Description of entrypoint. 32 | description?: string, 33 | 34 | /// Optional. Custom execution stack of interceptor names. 35 | executionStack?: string[], 36 | 37 | /// Optional. Display rank. 38 | displayRank?: number, 39 | 40 | /// Optional. Example requests. This is for human consumption. . 41 | exampleRequests?: Request[], 42 | 43 | /// Optional. Example responses. 44 | exampleResponses?: Response[] 45 | }; 46 | 47 | /// Interceptor definition. 48 | export interface InterceptorDefinition extends FunctionDefinition { 49 | /// _type === 'Interceptor' 50 | _type: "Interceptor", 51 | 52 | /// Optional. Description of interceptor 53 | description?: string, 54 | }; 55 | 56 | //////////////////////////////////////////////////////////////////////////////// 57 | /// Object constructors for built-in objects. 58 | 59 | /// Constructor for Function. 60 | export function createFunction( 61 | definition: FunctionDefinition, 62 | context: ObjectContext): Function { 63 | 64 | if (definition.function != null) { 65 | // Dynamicly created function. 66 | // TODO: do security check. 67 | return eval('(' + definition.function + ')'); 68 | } 69 | 70 | if (definition.moduleName != null && definition.functionName != null) { 71 | // create function from module and function name. 72 | let moduleName = definition.moduleName; 73 | if (moduleName.startsWith('.')) { 74 | moduleName = path.resolve(context.baseDir, moduleName); 75 | } 76 | return utils.appendMessageOnException("Unable to create function '" 77 | + definition.function 78 | + "' in module '" 79 | + definition.moduleName 80 | + "'.", 81 | () => { 82 | return utils.loadFunction(moduleName, definition.functionName); 83 | }); 84 | } 85 | throw new Error("Either property group 'moduleName' and 'functionName' or property 'function' should be present for Function object."); 86 | } 87 | 88 | /// Constructor for EntryPoint. 89 | export function createEntryPoint( 90 | definition: EntryPointDefinition, 91 | context: ObjectContext): EntryPoint { 92 | // TODO: any check? 93 | return createFunction(definition, context); 94 | } 95 | 96 | /// Constructor for Interceptor. 97 | export function createInterceptor( 98 | definition: InterceptorDefinition, 99 | context: ObjectContext): Interceptor { 100 | // TODO: any check? 101 | return createFunction(definition, context); 102 | } 103 | 104 | -------------------------------------------------------------------------------- /lib/builtins.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as types from './builtin-types'; 5 | import * as interceptors from './builtin-interceptors'; 6 | import * as entryPoints from './builtin-entrypoints'; 7 | 8 | export {types, entryPoints, interceptors}; -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////////////////////////// 5 | /// Winery.js facade 6 | 7 | // Export core entities in flattened namespace. 8 | export * from './application' 9 | export * from './object-model' 10 | export * from './request-context' 11 | export * from './request-template' 12 | export * from './request' 13 | export * from './response' 14 | 15 | export { Host, HostConfig, HostSettings } from "./host"; 16 | 17 | // Export misc entities in sub namespaces. 18 | import * as builtins from './builtins'; 19 | export { builtins }; 20 | 21 | import * as path from 'path'; 22 | import { HostConfig, Host, Hub } from './host'; 23 | 24 | /// A global host instance. 25 | let _host: Host = undefined; 26 | 27 | /// Create or get a hub. 28 | export function hub(): Host { 29 | if (_host == null) { 30 | _host = new Hub(HostConfig.fromConfig( 31 | path.resolve(__dirname, "../config/host.json"))); 32 | } 33 | return _host; 34 | } -------------------------------------------------------------------------------- /lib/metric.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////////////////////////// 5 | /// Metric support in Winery.js 6 | 7 | import * as path from 'path'; 8 | import {metric} from 'napajs'; 9 | import * as utils from './utils'; 10 | 11 | /// Interface for metric collection. 12 | export type MetricCollection = { [name: string]: metric.Metric }; 13 | 14 | /// Class for Metric definition. 15 | export interface MetricDef { 16 | /// Name used to access this metric via context.metric[''] 17 | name: string; 18 | 19 | /// Section e for the metric, which is passed to create the metric. 20 | sectionName: string; 21 | 22 | /// Display name for the metric, which is passed to create the metric. 23 | displayName: string; 24 | 25 | /// Description for this metric. For human consumption purpose. 26 | description?: string; 27 | 28 | /// Metric type. 29 | type: metric.MetricType; 30 | 31 | /// Dimension definitions. 32 | dimensionNames?: string[]; 33 | } 34 | 35 | const SCHEMA_DIR: string = path.resolve(__dirname, '../schema'); 36 | 37 | /// Helper class to read MetricDefinition array from config. 38 | export class MetricConfig { 39 | /// JSON schema used to validate config. 40 | private static readonly METRIC_CONFIG_SCHEMA: utils.JsonSchema 41 | = new utils.JsonSchema(path.resolve(SCHEMA_DIR, "metric-config.schema.json")); 42 | 43 | /// Transform object from JSON to object. 44 | private static _transform: utils.Transform = 45 | new utils.SetDefaultValue( { 46 | 'dimensionNames': [] 47 | }).add( 48 | new utils.TransformPropertyValues({ 49 | 'type': (metricTypeName: string) => { 50 | let lowerCaseTypeName = metricTypeName.toLowerCase(); 51 | switch (lowerCaseTypeName) { 52 | case 'number': return metric.MetricType.Number; 53 | case 'rate': return metric.MetricType.Rate; 54 | case 'percentile': return metric.MetricType.Percentile; 55 | default: throw new Error("Invalid metric type: '" + metricTypeName + "'."); 56 | } 57 | } 58 | })); 59 | 60 | /// Create MetricDefinition array from a JS object array that conform with schema. 61 | /// Throw exception if JS object array doesn't match schema. 62 | /// Schema: "../schema/metric-config.schema.json" 63 | /// 64 | /// Section name used to create counters. 65 | /// a JS value array to create MetricDefinition object. 66 | /// A list of NamedObjectDefinition objects. 67 | public static fromConfigObject(sectionName: string, jsValue: any[]): MetricDef[] { 68 | utils.ensureSchema(jsValue, this.METRIC_CONFIG_SCHEMA); 69 | 70 | jsValue.forEach(obj => { 71 | this._transform.apply(obj); 72 | obj.sectionName = sectionName; 73 | }); 74 | return (jsValue); 75 | } 76 | 77 | /// Create MetricDefinition array from a configuration file. (.config or .JSON) 78 | /// Throw exception if JS object array doesn't match schema. 79 | /// Schema: "../schema/metric-config.schema.json" 80 | /// 81 | /// a .config or .JSON file in metric definition schema. 82 | /// A list of MetricDefinition objects. 83 | public static fromConfig(sectionName: string, metricConfigFile: string): MetricDef[] { 84 | return utils.appendMessageOnException( 85 | "Error found in metric definition file '" + metricConfigFile + "'.", 86 | () => { return this.fromConfigObject(sectionName, utils.readConfig(metricConfigFile)); }); 87 | } 88 | } 89 | 90 | /// Create metric collection from metric definitions. 91 | export function createMetricCollection(defs: MetricDef[]): MetricCollection { 92 | let metrics: MetricCollection = {}; 93 | for (let m of defs) { 94 | metrics[m.name] = metric.get( 95 | m.sectionName, 96 | m.displayName, 97 | m.type, 98 | m.dimensionNames); 99 | } 100 | return metrics; 101 | } -------------------------------------------------------------------------------- /lib/named-object.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as path from 'path'; 5 | import * as utils from './utils'; 6 | 7 | import { ObjectContext } from './object-context'; 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | // Interfaces for Named Objects. 11 | // Named Objects are application-level objects with a well-known name, so user can retrive the object via app.getObject(''); 12 | // 13 | // There are two types of named objects. 14 | // 1) Named objects provided from JSON file per application, whose lifecycle is process- level. 15 | // 2) Named objects provided from a single Winery request, whose lifecycle is during the request. 16 | 17 | /// Class for named object that holds a definition and value. 18 | /// Definition is needed to construct this named object under a different ObjectContext, e.g, from request. 19 | /// 20 | 21 | /// Interface for Named object definition. 22 | export interface NamedObjectDef { 23 | /// Name or key to retrieve this object. 24 | name: string; 25 | 26 | /// Description for this object. 27 | description?: string; 28 | 29 | /// If this object is private, means that cannot be listed by entry points `listNamedObjects`. 30 | private?: boolean; 31 | 32 | /// If this object overrides a previous object definition with the same name. 33 | /// This may be useful if you borrow definition file from other apps and want to override individual ones. 34 | /// 35 | override?: boolean; 36 | 37 | /// Value of the input to create this named object, which is described by plain JavaScript object or URI. 38 | /// The plain JavaScript object / URI can be constructed by registered ObjectFactory and ObjectProvider. 39 | /// 40 | value: any; 41 | 42 | /// Dependency from current definition to object context. This is calculated automatically. 43 | dependencies?: ObjectContextDependency; 44 | } 45 | 46 | export interface NamedObject { 47 | /// Definition of current named object. 48 | def: NamedObjectDef; 49 | 50 | /// Value of current named object 51 | value: any; 52 | 53 | /// Scope of where this named object is provided. 54 | readonly scope: string; 55 | } 56 | 57 | /// Interface for Named Object collection. 58 | export interface NamedObjectCollection { 59 | /// Get named object by name. 60 | /// Name. Case-sensitive. 61 | /// Named object if found. Otherwise undefined. 62 | get(name: string): NamedObject; 63 | 64 | /// Iterator each object in this collection. 65 | forEach(callback: (object: NamedObject) => void): void; 66 | } 67 | 68 | 69 | /// An implementation of NamedObjectCollection based on name to object registry. 70 | export class NamedObjectRegistry implements NamedObjectCollection { 71 | /// Name to object map. Case sensitive. 72 | private _nameToObjectMap: Map = new Map(); 73 | 74 | /// Get object by name. 75 | /// Case sensitive name. 76 | /// undefined if not present, otherwise an instance of NamedObject. 77 | public get(name: string): NamedObject { 78 | return this._nameToObjectMap.get(name); 79 | } 80 | 81 | /// Tell if a name exists in this registry. 82 | /// True if exists, otherwise false. 83 | public has(name: string): boolean { 84 | return this._nameToObjectMap.has(name); 85 | } 86 | 87 | /// Iterate each object in this registry. 88 | public forEach(callback: (object: NamedObject) => void): void { 89 | this._nameToObjectMap.forEach(object => { 90 | callback(object); 91 | }); 92 | } 93 | 94 | /// Insert a named object. 95 | /// an Named object instance. 96 | public insert(object: NamedObject): void { 97 | this._nameToObjectMap.set(object.def.name, object); 98 | } 99 | 100 | /// Create NamedObjectRegistry from a collection of NamedObjectDefinition objects. 101 | /// Scope that current object definition apply to. Can be 'global', 'application', 'request', etc. 102 | /// Collection of NamedObjectDefinition objects. 103 | /// A list of ObjectContext objects. 104 | /// NamedObjectRegistry 105 | public static fromDefinition( 106 | scope: string, 107 | namedObjectDefCollection: NamedObjectDef[], 108 | context: ObjectContext): NamedObjectRegistry { 109 | 110 | let registry = new NamedObjectRegistry(); 111 | if (namedObjectDefCollection != null) { 112 | for (let def of namedObjectDefCollection) { 113 | let value = context.create(def.value); 114 | registry.insert({ 115 | def: def, 116 | value: value, 117 | scope: scope 118 | }); 119 | } 120 | } 121 | return registry; 122 | } 123 | } 124 | 125 | /// Dependency information on types, object and providers. 126 | /// When type, object, provider override happens at request time, 127 | /// We use this information to determine if a named object needs to be invalidated at request time. 128 | /// We only analyze dependency information for named objects registered at application level, 129 | /// as request level named object anyway will be re-created. 130 | /// 131 | export class ObjectContextDependency { 132 | private _dependentTypesNames: Set = new Set(); 133 | private _dependentObjectNames: Set = new Set(); 134 | private _dependentProtocolNames: Set = new Set(); 135 | 136 | /// Set a depenency on a object type 137 | public setTypeDependency(typeName: string) { 138 | this._dependentTypesNames.add(typeName); 139 | } 140 | 141 | /// Set a depenency on a URI protocol. 142 | public setProtocolDependency(protocolName: string) { 143 | this._dependentProtocolNames.add(protocolName); 144 | } 145 | 146 | /// Set a depenency on a named object. 147 | public setObjectDependency(objectName: string) { 148 | this._dependentObjectNames.add(objectName); 149 | } 150 | 151 | /// Get all dependent type names. 152 | public get typeDependencies(): Set { 153 | return this._dependentTypesNames; 154 | } 155 | 156 | /// Get all dependent URI protocol names. 157 | public get protocolDependencies(): Set { 158 | return this._dependentProtocolNames; 159 | } 160 | 161 | /// Get all dependent object names. 162 | public get objectDependencies(): Set { 163 | return this._dependentObjectNames; 164 | } 165 | }; 166 | 167 | 168 | const SCHEMA_DIR: string = path.resolve(__dirname, '../schema'); 169 | 170 | /// Helper class to read NamedObjectDefinition array from config. 171 | export class NamedObjectConfig { 172 | /// JSON schema used to validate conf. 173 | static readonly NAMED_OBJECT_CONFIG_SCHEMA: utils.JsonSchema = 174 | new utils.JsonSchema(path.resolve(SCHEMA_DIR, "named-object-config.schema.json")); 175 | 176 | /// Transform object from JSON to object. 177 | private static _transform: utils.Transform = 178 | new utils.SetDefaultValue({ 179 | 'override': false, 180 | 'private': false 181 | }); 182 | 183 | /// Create NamedObjectDefinition array from a JS object array that conform with schema. 184 | /// Throw exception if JS object array doesn't match schema. 185 | /// Schema: "../schema/named-object-config.schema.json" 186 | /// 187 | /// a JS value array to create NamedObjectDefinition object. 188 | /// Whether validate schema, 189 | /// this option is given due to request object already checked schema at request level. 190 | /// A list of NamedObjectDefinition objects. 191 | public static fromConfigObject(jsValue: any[], validateSchema: boolean = true): NamedObjectDef[]{ 192 | if (validateSchema) { 193 | utils.ensureSchema(jsValue, this.NAMED_OBJECT_CONFIG_SCHEMA); 194 | } 195 | 196 | jsValue.forEach(obj => { 197 | this._transform.apply(obj); 198 | }); 199 | return jsValue; 200 | } 201 | 202 | /// Create NamedObjectDefinition array from a configuration file. (.config or .json) 203 | /// Throw exception if configuration file parse failed or doesn't match schema. 204 | /// Schema: "../schema/named-object-config.schema.json" 205 | /// 206 | /// a JSON file in named object definition schema. 207 | /// A list of NamedObjectDefinition objects. 208 | public static fromConfig(namedObjectConfigFile: string): NamedObjectDef[] { 209 | return utils.appendMessageOnException( 210 | "Error found in named object definition file '" + namedObjectConfigFile + "'.", 211 | () => { return this.fromConfigObject(utils.readConfig(namedObjectConfigFile)); }); 212 | } 213 | } -------------------------------------------------------------------------------- /lib/object-model.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | // Facade for exporting object-model interfaces and classes. 5 | 6 | export * from './object-type'; 7 | export * from './object-provider'; 8 | export * from './named-object'; 9 | export * from './object-context'; 10 | -------------------------------------------------------------------------------- /lib/object-type.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ////////////////////////////////////////////////////////////////////////////////////// 5 | // Interfaces and classes for type based object creation. 6 | 7 | import * as path from 'path'; 8 | 9 | import * as utils from './utils'; 10 | import { ObjectContext } from './object-context'; 11 | 12 | /// Interface for objects with '_type' property, which is used to determine object creator. 13 | /// '_type' property is case-sensitive. 14 | /// 15 | export interface ObjectWithType { 16 | _type: string 17 | }; 18 | 19 | /// Object type definition to register a type in Winery. 20 | export interface TypeDef { 21 | /// Type name to apply this constructor. 22 | typeName: string; 23 | 24 | /// Description of this type. 25 | description?: string; 26 | 27 | /// Constructor module name. 28 | moduleName: string; 29 | 30 | /// Constructor function name. 31 | functionName: string; 32 | 33 | /// If this definition overrides a previous type definition with the same type name. 34 | /// This may be useful if you borrow definition file from other apps and want to override individual ones. 35 | /// 36 | override?: boolean; 37 | 38 | /// Example input object for human consumption. 39 | exampleObjects?: any[]; 40 | } 41 | 42 | /// Function interface for factory method, which takes an input object and produce an output object. 43 | export interface FactoryMethod { 44 | (input: ObjectWithType | ObjectWithType[], context?: ObjectContext): any 45 | } 46 | 47 | /// Interface for object factory. 48 | export interface ObjectFactory { 49 | 50 | /// Create an output JS value from input object. 51 | /// Object with '_type' property or object array. 52 | /// Context if needed to construct sub-objects. 53 | /// Any JS value type. 54 | /// 55 | /// When input is array, all items in array must be the same type. 56 | /// On implementation, you can check whether input is array or not as Array.isArray(input). 57 | /// Please refer to example\example_types.ts. 58 | /// 59 | create(input: ObjectWithType | ObjectWithType[], context?: ObjectContext): any; 60 | 61 | /// Check whether current object factory support given type. 62 | /// value of '_type' property. 63 | /// True if supported, else false. 64 | supports(typeName: string): boolean; 65 | } 66 | 67 | /// An implementation of ObjectFactory that allows to register type with their creator. 68 | export class TypeRegistry implements ObjectFactory { 69 | 70 | /// Type name to creator map. 71 | private _typeToFactoryMethod: Map = new Map(); 72 | 73 | /// Create an output object from input object. Exception will be thrown if type is not found in current application. 74 | /// Input object with a property '_type', the value of '_type' should be registered in current application. 75 | /// Object created from input. 76 | /// When input is array, all items in array must be the same type. 77 | public create(input: ObjectWithType | ObjectWithType[], context?: ObjectContext): any { 78 | if (input == null) { 79 | return null; 80 | } 81 | let typeName: string; 82 | if (Array.isArray(input)) { 83 | if ((input).length == 0) { 84 | return []; 85 | } 86 | // It assumes that all items in array are the same type. 87 | typeName = (input)[0]._type; 88 | for (let elem of input) { 89 | if (elem._type !== typeName) { 90 | throw new Error("Property '_type' must be the same for all elements in input array when calling ObjectFactory.create."); 91 | } 92 | } 93 | } 94 | else { 95 | typeName = (input)._type; 96 | } 97 | 98 | if (this.supports(typeName)) { 99 | return this._typeToFactoryMethod.get(typeName)(input, context); 100 | } 101 | throw new Error("Not supported type: '" + typeName + "'."); 102 | } 103 | 104 | /// Register an object creator for a type. Later call to this method on the same type will override the creator of former call. 105 | /// Case sensitive type name. 106 | /// Function that takes one object as input and returns an object. 107 | public register(type: string, creator: FactoryMethod): void { 108 | this._typeToFactoryMethod.set(type, creator); 109 | } 110 | 111 | /// Check if current type registry contain a type. 112 | /// Case sensitive type name. 113 | /// True if type is registered, otherwise false. 114 | public supports(typeName: string): boolean { 115 | return this._typeToFactoryMethod.has(typeName); 116 | } 117 | 118 | /// Create TypeRegistry from a collection of TypeDefinitions 119 | /// A collection of type definitions 120 | /// Base directory name according to which module name will be resolved. 121 | /// A TypeRegistry object. 122 | public static fromDefinition(typeDefCollection: TypeDef[], baseDir: string): TypeRegistry { 123 | let registry = new TypeRegistry(); 124 | if (typeDefCollection != null) { 125 | typeDefCollection.forEach(def => { 126 | let moduleName = def.moduleName; 127 | if (def.moduleName.startsWith(".")) { 128 | moduleName = path.resolve(baseDir, moduleName); 129 | } 130 | let creator = utils.loadFunction(moduleName, def.functionName); 131 | registry.register(def.typeName, creator); 132 | }); 133 | } 134 | return registry; 135 | } 136 | } 137 | 138 | 139 | const SCHEMA_DIR: string = path.resolve(__dirname, '../schema'); 140 | 141 | /// Helper class to read TypeDefinition array from config. 142 | export class TypeConfig { 143 | /// JSON schema used to validate conf. 144 | private static readonly OBJECT_TYPE_CONFIG_SCHEMA: utils.JsonSchema = 145 | new utils.JsonSchema(path.resolve(SCHEMA_DIR, "object-type-config.schema.json")); 146 | 147 | /// Transforms from config object to definition. 148 | private static _transform: utils.Transform = 149 | new utils.SetDefaultValue({ 150 | 'override': false 151 | }); 152 | 153 | /// Create TypeDefinition array from a JS object array that conform with schema. 154 | /// Throw exception if JS object array doesn't match schema. 155 | /// Schema: "../schema/object-type-config.schema.json" 156 | /// 157 | /// a JS object array to create TypeDefinition object. 158 | /// Whether validate schema, 159 | /// this option is given due to request object already checked schema at request level. 160 | /// A list of TypeDefinition objects. 161 | public static fromConfigObject(jsValue: any[], validateSchema: boolean = true): TypeDef[] { 162 | if (validateSchema) { 163 | utils.ensureSchema(jsValue, this.OBJECT_TYPE_CONFIG_SCHEMA); 164 | } 165 | 166 | jsValue.forEach(obj => { 167 | this._transform.apply(obj); 168 | }); 169 | return jsValue; 170 | } 171 | 172 | /// From a config file to create a array of TypeDefinition. 173 | /// TypeDefinition config file. 174 | /// A list of TypeDefinition objects. 175 | public static fromConfig(objectTypeConfig: string): TypeDef[] { 176 | return utils.appendMessageOnException( 177 | "Error found in object type definition file '" + objectTypeConfig + "'.", 178 | () => { return this.fromConfigObject(utils.readConfig(objectTypeConfig)); }); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/request-template.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////// 5 | /// Interfaces and classes for request template support in Winery.js 6 | 7 | import * as assert from 'assert'; 8 | import * as path from 'path'; 9 | 10 | import * as utils from './utils'; 11 | 12 | import { Application } from './application'; 13 | import { 14 | ScopedObjectContextDef, 15 | ScopedObjectContext, 16 | TypeDef, 17 | ProviderDef, 18 | NamedObjectDef 19 | } from './object-model'; 20 | 21 | import * as fs from 'fs'; 22 | 23 | /// Request template definition. 24 | export interface RequestTemplateDef { 25 | /// Uri of base template to inherit from. 26 | /// This is required if property 'application' is not present. 27 | /// 28 | base?: string; 29 | 30 | /// Application instance name this template applies to. 31 | /// This is required if property 'base' is not present. 32 | /// 33 | application?: string; 34 | 35 | /// Definition of overridden types 36 | overrideTypes?: TypeDef[]; 37 | 38 | /// Definition of overridden providers 39 | overrideProviders?: ProviderDef[]; 40 | 41 | /// Definition of overridden named objects 42 | overrideObjects?: NamedObjectDef[]; 43 | } 44 | 45 | /// Request template. 46 | export class RequestTemplate { 47 | /// Uri of this template. 48 | private _uri: string; 49 | 50 | /// Request-template level object context. 51 | private _objectContext: ScopedObjectContext; 52 | 53 | /// Application to which this template applies. 54 | private _app: Application; 55 | 56 | /// Base template. 57 | private _base: RequestTemplate; 58 | 59 | /// Constructor. 60 | public constructor( 61 | uri: string, 62 | app: Application, 63 | base: RequestTemplate, 64 | def: RequestTemplateDef) { 65 | 66 | assert(uri != null); 67 | assert(app != null); 68 | 69 | this._uri = uri; 70 | this._app = app; 71 | this._base = base; 72 | 73 | // Create request-template level object context. 74 | let parentContext: ScopedObjectContext = 75 | base != null ? base.objectContext : app.objectContext; 76 | 77 | let contextDef = new ScopedObjectContextDef( 78 | parentContext.def, 79 | def.overrideTypes, 80 | def.overrideProviders, 81 | def.overrideObjects, 82 | true 83 | ); 84 | 85 | this._objectContext = new ScopedObjectContext( 86 | `template:${uri}`, 87 | app.objectContext.baseDir, // We always use application base dir to resolve paths in request template definition. 88 | parentContext, 89 | contextDef 90 | ); 91 | } 92 | 93 | /// Get Uri of this request template. 94 | public get uri(): string { 95 | return this._uri; 96 | } 97 | 98 | /// Get application to which this template applies. 99 | public get application(): Application { 100 | return this._app; 101 | } 102 | 103 | /// Get base template. 104 | public get base(): RequestTemplate { 105 | return this._base; 106 | } 107 | 108 | /// Get template-level object context. 109 | public get objectContext() : ScopedObjectContext { 110 | return this._objectContext; 111 | } 112 | } 113 | 114 | /// Interface for RequestTemplate loader by ID. 115 | export interface RequestTemplateLoader { 116 | /// Load a request template by uri. 117 | /// It will try to get a base by baseTemplateGetter first, and load the base recursively if not found. 118 | /// 119 | load(uri: string, 120 | applicationGetter: (app: string) => Application, 121 | baseTemplateGetter: (uri: string) => RequestTemplate): RequestTemplate; 122 | 123 | /// Get applied application name of a template URI without loading it. 124 | getApplicationName(uri: string): string; 125 | } 126 | 127 | /// Loader for file based request template. 128 | export class RequestTemplateFileLoader { 129 | /// JSON schema used to validate config. 130 | private static readonly REQUEST_TEMPLATE_SCHEMA: utils.JsonSchema 131 | = new utils.JsonSchema(path.resolve(__dirname, "../schema/request-template.schema.json")); 132 | 133 | public load(uri: string, 134 | appGetter: (app: string) => Application, 135 | baseTemplateGetter: (baseUri: string) => RequestTemplate) : RequestTemplate { 136 | 137 | let seenUris: { [uri: string]: boolean} = {}; 138 | return this.loadInternal(uri, appGetter, baseTemplateGetter, seenUris); 139 | } 140 | 141 | private loadInternal(uri: string, 142 | appGetter: (app: string) => Application, 143 | baseTemplateGetter: (baseUri: string) => RequestTemplate, 144 | seenUris: {[uri: string] : boolean}) : RequestTemplate { 145 | 146 | let lcUri = uri.toLowerCase(); 147 | if (seenUris[lcUri] != null) { 148 | throw new Error(`Circle found in template inheritance. Uri: "${uri}"`); 149 | } 150 | seenUris[lcUri] = true; 151 | 152 | let def = utils.readConfig(uri, RequestTemplateFileLoader.REQUEST_TEMPLATE_SCHEMA); 153 | let baseDir = path.dirname(uri); 154 | 155 | let base: RequestTemplate = undefined; 156 | let app: Application = undefined; 157 | if (def.base != null) { 158 | let basePath = def.base; 159 | if (def.base.startsWith('.')) { 160 | basePath = path.resolve(baseDir, def.base); 161 | } else { 162 | basePath = require.resolve(def.base); 163 | } 164 | base = this.loadInternal( 165 | basePath, 166 | appGetter, 167 | baseTemplateGetter, 168 | seenUris); 169 | 170 | app = base.application; 171 | } 172 | else { 173 | if (def.application == null) { 174 | throw new Error(`Property "application" or "base" must be present in Request Template definition.`); 175 | } 176 | app = appGetter(def.application); 177 | if (app == null) { 178 | throw new Error(`Application "${def.application}" is not registered in current host.`); 179 | } 180 | } 181 | return new RequestTemplate(uri, app, base, def); 182 | } 183 | 184 | public getApplicationName(uri: string): string { 185 | let def = utils.readConfig(uri, 186 | RequestTemplateFileLoader.REQUEST_TEMPLATE_SCHEMA); 187 | 188 | if (def["base"] != null) { 189 | 190 | return this.getApplicationName( 191 | path.resolve(path.dirname(uri), def.base)); 192 | } 193 | else { 194 | assert(def.application != null); 195 | return def.application; 196 | } 197 | } 198 | } 199 | 200 | /// Request template reference. 201 | class RequestTemplateReference { 202 | public template: RequestTemplate; 203 | public refCount: number; 204 | 205 | constructor(template: RequestTemplate, refCount: number) { 206 | this.template = template; 207 | this.refCount = refCount; 208 | } 209 | } 210 | 211 | /// Request template manager. 212 | export class RequestTemplateManager { 213 | private _loader: RequestTemplateLoader; 214 | private _appGetter: (name: string) => Application; 215 | private _cache: Map = new Map(); 216 | 217 | /// Constructor. 218 | public constructor(loader: RequestTemplateLoader, 219 | appGetter: (name: string) => Application) { 220 | this._loader = loader; 221 | this._appGetter = appGetter; 222 | } 223 | 224 | /// Get a pre-loaded request template by ID. 225 | public get(uri: string): RequestTemplate { 226 | if (this._cache.has(uri)) { 227 | return this._cache.get(uri).template; 228 | } 229 | return undefined; 230 | } 231 | 232 | /// Load a request template by ID, and insert it into cache. 233 | private load(uri: string) : RequestTemplate { 234 | let thisTemplate = this._loader.load( 235 | uri, 236 | this._appGetter, 237 | (baseUri: string): RequestTemplate => { 238 | return this.get(baseUri); 239 | }); 240 | 241 | // Cache entire chain from current to top-most base using reference counting. 242 | let t = thisTemplate; 243 | while (t != null) { 244 | let uri = t.uri; 245 | if (this._cache.has(uri)) { 246 | let ref = this._cache.get(uri); 247 | ref.refCount++; 248 | } else { 249 | this._cache.set(uri, new RequestTemplateReference(t, 1)); 250 | } 251 | t = t.base; 252 | } 253 | return thisTemplate; 254 | } 255 | 256 | /// Get a pre-loaded request template or load it and return. 257 | public getOrLoad(uri: string): RequestTemplate { 258 | let t = this.get(uri); 259 | if (t == null) { 260 | return this.load(uri); 261 | } 262 | return t; 263 | } 264 | 265 | /// Unload a pre-loaded request template. 266 | public unload(uri: string): void { 267 | if (!this._cache.has(uri)) { 268 | return; 269 | } 270 | // Decrease reference entire chain from current to top-most base. 271 | // Unload if no further reference. 272 | while (true) { 273 | assert(this._cache.has(uri)); 274 | let r = this._cache.get(uri); 275 | 276 | if (--r.refCount == 0) { 277 | this._cache.delete(uri); 278 | } 279 | if (r.template.base == null) { 280 | break; 281 | } 282 | uri = r.template.base.uri; 283 | } 284 | } 285 | 286 | /// Return loaded templates. 287 | public get loadedTemplates(): string[] { 288 | let uris: string[] = []; 289 | this._cache.forEach((r: RequestTemplateReference, uri: string) => { 290 | uris.push(uri); 291 | }); 292 | return uris; 293 | } 294 | } -------------------------------------------------------------------------------- /lib/request.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////// 5 | /// Interface and classes for Winery.js request object 6 | 7 | import * as path from 'path'; 8 | import * as utils from './utils'; 9 | 10 | import { TypeDef, ProviderDef, NamedObjectDef }from './object-model'; 11 | 12 | /// Interface for control flags. 13 | export type ControlFlags = { 14 | /// Enable debugging or not. Set to false by default. 15 | debug?: boolean; 16 | 17 | /// Return performance numbers or not. Set to false by default. 18 | perf?: boolean; 19 | } 20 | 21 | /// Interface for winery request. 22 | export interface Request { 23 | /// Registered application instance name. Required unless "base" is present. 24 | application?: string; 25 | 26 | /// Uri for request template to apply. Optional. 27 | base?: string 28 | 29 | /// Entry point name 30 | entryPoint: string; 31 | 32 | /// Trace ID 33 | traceId?: string; 34 | 35 | /// User input as the 1st argument passing to entry point function 36 | input?: any; 37 | 38 | /// Control flags 39 | controlFlags?: ControlFlags; 40 | 41 | /// Overridden types 42 | overrideTypes?: TypeDef[]; 43 | 44 | /// Overridden named objects 45 | overrideObjects?: NamedObjectDef[]; 46 | 47 | /// Overridden providers 48 | overrideProviders?: ProviderDef[]; 49 | } 50 | 51 | /// Request helper. 52 | export class RequestHelper { 53 | /// JSON schema for resquest. 54 | private static readonly REQUEST_SCHEMA: utils.JsonSchema = new utils.JsonSchema( 55 | path.resolve(path.resolve(__dirname, '../schema'), "request.schema.json")); 56 | 57 | /// Set default values transform. 58 | private static _transform = new utils.SetDefaultValue({ 59 | traceId: "Unknown", 60 | overrideObjects: [], 61 | overrideProviders: [], 62 | overrideTypes: [], 63 | controlFlags: { 64 | debug: false, 65 | perf: false 66 | } 67 | }); 68 | 69 | /// Tell if a jsValue is a valid request at run time. 70 | public static validate(jsValue: any): boolean { 71 | return this.REQUEST_SCHEMA.validate(jsValue); 72 | } 73 | 74 | /// Create request from a JS value that conform with request schema. 75 | public static fromJsValue(jsValue: any): Request { 76 | if (!this.validate(jsValue)) 77 | throw new Error(`Request doesn't match request schema: ${JSON.stringify(this.REQUEST_SCHEMA.getErrors())}`); 78 | 79 | let request = (jsValue); 80 | this._transform.apply(request); 81 | // TODO: @dapeng, make SetDefaultValue recursive. 82 | if (request.controlFlags.debug == null) { 83 | request.controlFlags.debug = false; 84 | } 85 | 86 | if (request.controlFlags.perf == null) { 87 | request.controlFlags.perf = false; 88 | } 89 | 90 | return request; 91 | } 92 | } -------------------------------------------------------------------------------- /lib/response.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | ///////////////////////////////////////////////////////////////////// 5 | /// Interface and classes for Winery.js response object 6 | 7 | import * as utils from './utils'; 8 | import * as path from 'path'; 9 | 10 | /// Response code 11 | export enum ResponseCode { 12 | // Success. 13 | Success = 0, 14 | 15 | // Internal error. 16 | InternalError = 1, 17 | 18 | // Server side timeout. 19 | ProcessTimeout = 2, 20 | 21 | // Throttled due to policy. 22 | Throttled = 3, 23 | 24 | // Error caused by bad input. 25 | InputError = 4 26 | } 27 | 28 | /// Exception information in response. 29 | export type ExceptionInfo = { 30 | stack: string; 31 | message: string; 32 | fileName?: string; 33 | lineNumber?: number; 34 | columnNumber?: number; 35 | } 36 | 37 | /// Debug event in DebugInfo. 38 | export type DebugEvent = { 39 | eventTime: Date; 40 | logLevel: string; 41 | message: string; 42 | } 43 | 44 | /// Debug information when debug flag is on. 45 | export type DebugInfo = { 46 | exception: ExceptionInfo; 47 | events: DebugEvent[]; 48 | details: { [key: string]: any }; 49 | machineName: string; 50 | } 51 | 52 | /// Write performance numbers when perf flag is on. 53 | export type PerfInfo = { [perfKey: string]: number }; 54 | 55 | /// Interface for response 56 | export interface Response { 57 | /// Response code 58 | responseCode: ResponseCode; 59 | 60 | /// Error message if response code is not Success. 61 | errorMessage?: string; 62 | 63 | /// Output from entrypoint. 64 | output?: any; 65 | 66 | /// Debug information. 67 | debugInfo?: DebugInfo; 68 | 69 | /// Performance numbers. 70 | perfInfo?: PerfInfo; 71 | } 72 | 73 | /// Response helper. 74 | export class ResponseHelper { 75 | /// JSON schema for response. 76 | private static readonly RESPONSE_SCHEMA: utils.JsonSchema = new utils.JsonSchema( 77 | path.resolve(path.resolve(__dirname, '../schema'), "response.schema.json")); 78 | 79 | /// Parse a JSON string that conform with response schema. 80 | public static parse(jsonString: string): Response { 81 | let response = utils.parseJsonString(jsonString, this.RESPONSE_SCHEMA); 82 | return (response); 83 | } 84 | 85 | /// Validate a JS value against response schema. 86 | public static validate(jsValue: any): boolean { 87 | return this.RESPONSE_SCHEMA.validate(jsValue); 88 | } 89 | } -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "experimentalDecorators": true, 6 | "noImplicitAny": true, 7 | "declaration": true, 8 | "sourceMap": false, 9 | "lib": ["es2015"], 10 | "declarationDir": "../types" 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "winery", 3 | "version": "0.1.1", 4 | "author": "napajs", 5 | "description": "A framework for experimentation in production", 6 | "keywords": [ 7 | "experimentation", 8 | "A/B testing", 9 | "application framework", 10 | "dependency injection", 11 | "continuous modification" 12 | ], 13 | "main": "./lib/index.js", 14 | "types": "./types/index.d.ts", 15 | "readme": "./README.md", 16 | "dependencies": { 17 | "napajs": "^0.2.2", 18 | "strip-json-comments": "^2.0.0" 19 | }, 20 | "devDependencies": { 21 | "mocha": "^3.5.0", 22 | "typescript": "^2.4.2", 23 | "@types/node": "^8.0.22", 24 | "@types/mocha": "^2.2.41" 25 | }, 26 | "scripts": { 27 | "prepare": "tsc -p lib && tsc -p test && tsc -p examples/playground", 28 | "test": "mocha test --recursive" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /schema/application-config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/application-config", 4 | "type": "object", 5 | "title": "Vineyard application configuration schema.", 6 | "description": "This JSON schema defines the root JSON file to define a application.", 7 | "name": "/", 8 | "properties": { 9 | "id": { 10 | "id": "http://www.wineryjs.org/application-config/id", 11 | "type": "string", 12 | "title": "Application ID.", 13 | "description": "Required. Application id which is unique in Winery.", 14 | "name": "id" 15 | }, 16 | "description": { 17 | "id": "http://www.wineryjs.org/application-config/description", 18 | "type": "string", 19 | "title": "Application description.", 20 | "description": "Required. Purpose and scope of this application.", 21 | "name": "description" 22 | }, 23 | "allowPerRequestOverride": { 24 | "id": "http://www.wineryjs.org/object-type-config/item/allowPerRequestOverride", 25 | "type": "boolean", 26 | "title": "Allow per-request override schema.", 27 | "description": "Whether to allow per-request override for this application.", 28 | "name": "allowPerRequestOverride", 29 | "default": true 30 | }, 31 | "defaultExecutionStack": { 32 | "id": "http://www.wineryjs.org/application-config/defaultExecutionStack", 33 | "type": "array", 34 | "title": "Default execution stack consists of a list of interceptor names.", 35 | "description": "Optional. If not specified, will inherit from Host configuration.", 36 | "name": "defaultExecutionStack", 37 | "items": { 38 | "id": "http://www.wineryjs.org/application-config/defaultExecutionStack/0", 39 | "type": "string", 40 | "title": "Interceptor name", 41 | "description": "Interceptor are stacked as execution stack. ." 42 | } 43 | }, 44 | "objectTypes": { 45 | "id": "http://www.wineryjs.org/application-config/objectTypes", 46 | "type": "array", 47 | "title": "Object type definition files.", 48 | "description": "Required. Object types supported in this application.", 49 | "name": "objectTypes", 50 | "items": { 51 | "id": "http://www.wineryjs.org/application-config/objectTypes/0", 52 | "type": "string", 53 | "title": "Path of an object type definition file", 54 | "description": "Multiple object type definition file is supported." 55 | } 56 | }, 57 | "namedObjects": { 58 | "id": "http://www.wineryjs.org/application-config/namedObjects", 59 | "type": "array", 60 | "title": "Named objects definition files.", 61 | "description": "Required. Named objects are objects that you want to access them by app.getObject('').", 62 | "name": "namedObjects", 63 | "items": { 64 | "id": "http://www.wineryjs.org/application-config/namedObjects/0", 65 | "type": "string", 66 | "title": "Path of a named object definition file.", 67 | "description": "Multiple named object definition file is supported." 68 | } 69 | }, 70 | "objectProviders": { 71 | "id": "http://www.wineryjs.org/application-config/objectProviders", 72 | "type": "array", 73 | "title": "Object providers definition files.", 74 | "description": "Optional. Object providers are functions that provide objects by URI.", 75 | "name": "objectProviders", 76 | "items": { 77 | "id": "http://www.wineryjs.org/application-config/objectProviders/0", 78 | "type": "string", 79 | "title": "Path of an object provider definition file.", 80 | "description": "Multiple object provider definition file is supported." 81 | } 82 | }, 83 | "metrics": { 84 | "id": "http://www.wineryjs.org/application-config/metrics", 85 | "type": "object", 86 | "title": "Metrics collection definition.", 87 | "description": "Performance metricss definition for current application.", 88 | "name": "metricss", 89 | "additionalProperties": false, 90 | "properties": { 91 | "sectionName": { 92 | "id": "http://www.wineryjs.org/application-config/metrics/sectionName", 93 | "type": "string", 94 | "title": "Section name.", 95 | "description": "Section name of performance metrics.", 96 | "name": "sectionName" 97 | }, 98 | "definition": { 99 | "id": "http://www.wineryjs.org/application-config/metricss/definition", 100 | "type": "array", 101 | "title": "Definition files", 102 | "description": "A list of metrics definition files.", 103 | "name": "definition", 104 | "items": { 105 | "id": "http://www.wineryjs.org/application-config/metricss/definition/0", 106 | "type": "string", 107 | "title": "Path of a metrics definition file", 108 | "description": "Multiple metrics definition file is supported." 109 | } 110 | } 111 | }, 112 | "required": [ "sectionName", "definition"] 113 | } 114 | }, 115 | "additionalProperties": false, 116 | "required": [ 117 | "id", 118 | "description", 119 | "namedObjects" 120 | ] 121 | } -------------------------------------------------------------------------------- /schema/host-config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/host-config", 4 | "type": "object", 5 | "title": "Vineyard application configuration schema.", 6 | "description": "This JSON schema defines the root JSON file to define a application.", 7 | "name": "/", 8 | "properties": { 9 | "allowPerRequestOverride": { 10 | "id": "http://www.wineryjs.org/object-type-config/item/allowPerRequestOverride", 11 | "type": "boolean", 12 | "title": "Allow per-request override schema.", 13 | "description": "Whether to allow per-request override for this application.", 14 | "name": "allowPerRequestOverride", 15 | "default": true 16 | }, 17 | "throwExceptionOnError": { 18 | "id": "http://www.wineryjs.org/object-type-config/item/throwExceptionOnError", 19 | "type": "boolean", 20 | "title": "Throw exception on error schema.", 21 | "description": "Whether to throw exception on error, or return error response code.", 22 | "name": "throwExceptionOnError", 23 | "default": true 24 | }, 25 | "defaultExecutionStack": { 26 | "id": "http://www.wineryjs.org/host-config/defaultExecutionStack", 27 | "type": "array", 28 | "title": "Default execution stack consists of a list of interceptor names.", 29 | "description": "Optional. If not specified, will inherit from host configuration.", 30 | "name": "defaultExecutionStack", 31 | "items": { 32 | "id": "http://www.wineryjs.org/host-config/defaultExecutionStack/0", 33 | "type": "string", 34 | "title": "Interceptor name", 35 | "description": "Interceptor are stacked as execution stack. ." 36 | } 37 | }, 38 | "objectTypes": { 39 | "id": "http://www.wineryjs.org/host-config/objectTypes", 40 | "type": "array", 41 | "title": "Object type definition files.", 42 | "description": "Required. Object types supported in this application.", 43 | "name": "objectTypes", 44 | "items": { 45 | "id": "http://www.wineryjs.org/host-config/objectTypes/0", 46 | "type": "string", 47 | "title": "Path of an object type definition file", 48 | "description": "Multiple object type definition file is supported." 49 | } 50 | }, 51 | "namedObjects": { 52 | "id": "http://www.wineryjs.org/host-config/namedObjects", 53 | "type": "array", 54 | "title": "Named objects definition files.", 55 | "description": "Required. Named objects are objects that you want to access them by app.getObject('').", 56 | "name": "namedObjects", 57 | "items": { 58 | "id": "http://www.wineryjs.org/host-config/namedObjects/0", 59 | "type": "string", 60 | "title": "Path of a named object definition file.", 61 | "description": "Multiple named object definition file is supported." 62 | } 63 | }, 64 | "objectProviders": { 65 | "id": "http://www.wineryjs.org/host-config/objectProviders", 66 | "type": "array", 67 | "title": "Object providers definition files.", 68 | "description": "Optional. Object providers are functions that provide objects by URI.", 69 | "name": "objectProviders", 70 | "items": { 71 | "id": "http://www.wineryjs.org/host-config/objectProviders/0", 72 | "type": "string", 73 | "title": "Path of an object provider definition file.", 74 | "description": "Multiple object provider definition file is supported." 75 | } 76 | } 77 | }, 78 | "additionalProperties": false, 79 | "required": [ 80 | "allowPerRequestOverride", 81 | "throwExceptionOnError", 82 | "defaultExecutionStack", 83 | "objectTypes", 84 | "namedObjects" 85 | ] 86 | } -------------------------------------------------------------------------------- /schema/metric-config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/metric-config", 4 | "type": "array", 5 | "title": "Metric config schema.", 6 | "description": "Metric config file is a sub file of application config in Winery.", 7 | "name": "/", 8 | "items": { 9 | "id": "http://www.wineryjs.org/metric-config/item", 10 | "type": "object", 11 | "title": "Metric definition schema.", 12 | "description": "An explanation about the puropose of this instance described by this schema.", 13 | "name": "item", 14 | "properties": { 15 | "name": { 16 | "id": "http://www.wineryjs.org/metric-config/item/name", 17 | "type": "string", 18 | "title": "Name schema.", 19 | "description": "Metric variable name accessed via app.counters.. Please use identifier for this property.", 20 | "name": "name" 21 | }, 22 | "displayName": { 23 | "id": "http://www.wineryjs.org/metric-config/item/displayName", 24 | "type": "string", 25 | "title": "Display name schema.", 26 | "description": "Display name for the metric, which will be displayed in monitoring tools.", 27 | "name": "displayName" 28 | }, 29 | "description": { 30 | "id": "http://www.wineryjs.org/metric-config/item/description", 31 | "type": "string", 32 | "title": "Description schema.", 33 | "description": "Description of this metric.", 34 | "name": "description" 35 | }, 36 | "type": { 37 | "id": "http://www.wineryjs.org/metric-config/item/type", 38 | "enum": [ "Number", "Rate", "Percentile" ], 39 | "title": "Metric type schema.", 40 | "description": "An explanation about the puropose of this instance described by this schema.", 41 | "name": "type" 42 | }, 43 | "dimensionNames": { 44 | "id": "http://www.wineryjs.org/metric-config/global/counters/dimensionNames", 45 | "type": "array", 46 | "title": "Dimension Names", 47 | "description": "List of dimension names being recorded with every metric sample.", 48 | "name": "dimensionNames", 49 | "items": { 50 | "id": "http://www.wineryjs.org/metric-config/global/counters/dimensionNames/item", 51 | "type": "string", 52 | "title": "Dimension name.", 53 | "description": "Dimension name metric.", 54 | "name": "item" 55 | } 56 | } 57 | }, 58 | "additionalProperties": false, 59 | "required": [ 60 | "name", 61 | "displayName", 62 | "type", 63 | "dimensionNames" 64 | ] 65 | } 66 | } -------------------------------------------------------------------------------- /schema/named-object-config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/named-object-config", 4 | "type": "array", 5 | "title": "Named object config schema.", 6 | "description": "Named object config as a sub-file of app config in Winery.", 7 | "name": "/", 8 | "items": { 9 | "id": "http://www.wineryjs.org/named-object-config/item", 10 | "type": "object", 11 | "title": "Named object definition schema.", 12 | "description": "Named object definition.", 13 | "name": "item", 14 | "properties": { 15 | "name": { 16 | "id": "http://www.wineryjs.org/named-object-config/item/name", 17 | "type": "string", 18 | "title": "Name schema.", 19 | "description": "Well-known name in current application.", 20 | "name": "name" 21 | }, 22 | "override": { 23 | "id": "http://www.wineryjs.org/named-object-config/item/override", 24 | "type": "boolean", 25 | "title": "Override schema.", 26 | "description": "If this named object will override the object with the same name declared before this definition.", 27 | "name": "override", 28 | "default": false 29 | }, 30 | "description": { 31 | "id": "http://www.wineryjs.org/named-object-config/item/description", 32 | "type": "string", 33 | "title": "Description schema.", 34 | "description": "Description of this object.", 35 | "name": "description" 36 | }, 37 | "private": { 38 | "id": "http://www.wineryjs.org/named-object-config/item/private", 39 | "type": "boolean", 40 | "title": "Private schema.", 41 | "description": "If this object is private.", 42 | "name": "private" 43 | }, 44 | "value": { 45 | "id": "http://www.wineryjs.org/named-object-config/item/value", 46 | "title": "Value schema.", 47 | "description": "Value of current object, can be any JS object, or object with _type property or URI.", 48 | "name": "value" 49 | } 50 | }, 51 | "additionalProperties": false, 52 | "required": [ 53 | "name", 54 | "value" 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /schema/object-provider-config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/object-provider-config", 4 | "type": "array", 5 | "title": "Object provider config Schema.", 6 | "description": "Object provider config as a sub file of app config in Winery.", 7 | "name": "/", 8 | "items": { 9 | "id": "http://www.wineryjs.org/object-provider-config/item", 10 | "type": "object", 11 | "title": "Object provider definition schema.", 12 | "description": "Object provider definition describes protocol name and object loading function.", 13 | "name": "item", 14 | "properties": { 15 | "protocol": { 16 | "id": "http://www.wineryjs.org/object-provider-config/item/protocol", 17 | "type": "string", 18 | "title": "Protocol schema.", 19 | "description": "Protocol name that current provider is responsible.", 20 | "name": "protocol" 21 | }, 22 | "override": { 23 | "id": "http://www.wineryjs.org/object-provider-config/item/override", 24 | "type": "boolean", 25 | "title": "Override schema.", 26 | "description": "If this provider will override the provider for the same protocol name declared before this definition.", 27 | "name": "override", 28 | "default": false 29 | }, 30 | "description": { 31 | "id": "http://www.wineryjs.org/object-provider-config/item/description", 32 | "type": "string", 33 | "title": "Description schema.", 34 | "description": "Description of current protocol.", 35 | "name": "description" 36 | }, 37 | "exampleUri": { 38 | "id": "http://www.wineryjs.org/object-provider-config/item/exampleUri", 39 | "type": "array", 40 | "title": "Example URI.", 41 | "description": "Example of URIs", 42 | "name": "exampleUri", 43 | "items": { 44 | "id": "http://www.wineryjs.org/object-provider-config/item/exampleUri/item", 45 | "type": "string", 46 | "title": "example Uri item.", 47 | "description": "Example Uri for this protocol.", 48 | "name": "item" 49 | } 50 | }, 51 | "moduleName": { 52 | "id": "http://www.wineryjs.org/object-provider-config/item/moduleName", 53 | "type": "string", 54 | "title": "Module name schema.", 55 | "description": "Module name for the provider.", 56 | "name": "moduleName" 57 | }, 58 | "functionName": { 59 | "id": "http://www.wineryjs.org/object-provider-config/item/functionName", 60 | "type": "string", 61 | "title": "Function name schema.", 62 | "description": "Function name for the provisioning function.", 63 | "name": "functionName" 64 | } 65 | }, 66 | "additionalProperties": false, 67 | "required": [ 68 | "protocol", 69 | "description", 70 | "exampleUri", 71 | "moduleName", 72 | "functionName" 73 | ] 74 | } 75 | } -------------------------------------------------------------------------------- /schema/object-type-config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/object-type-config", 4 | "type": "array", 5 | "title": "Type config schema in Winery.", 6 | "description": "Type config is a sub config file under app config in Winery.", 7 | "name": "/", 8 | "items": { 9 | "id": "http://www.wineryjs.org/object-type-config/item", 10 | "type": "object", 11 | "title": "Type definition schema.", 12 | "description": "Type definition describes type name and where the constructor locates.", 13 | "name": "item", 14 | "properties": { 15 | "typeName": { 16 | "id": "http://www.wineryjs.org/object-type-config/item/typeName", 17 | "type": "string", 18 | "title": "Type schema.", 19 | "description": "Type name, case-sensitive.", 20 | "name": "typeName" 21 | }, 22 | "override": { 23 | "id": "http://www.wineryjs.org/object-type-config/item/override", 24 | "type": "boolean", 25 | "title": "Override schema.", 26 | "description": "If this type will override the same type name declared before this definition..", 27 | "name": "override", 28 | "default": false 29 | }, 30 | "description": { 31 | "id": "http://www.wineryjs.org/object-type-config/item/description", 32 | "type": "string", 33 | "title": "Description schema.", 34 | "description": "Description of the type.", 35 | "name": "description" 36 | }, 37 | "moduleName": { 38 | "id": "http://www.wineryjs.org/object-type-config/item/moduleName", 39 | "type": "string", 40 | "title": "Module name schema.", 41 | "description": "Module name of the constructor.", 42 | "name": "moduleName" 43 | }, 44 | "functionName": { 45 | "id": "http://www.wineryjs.org/object-type-config/item/functionName", 46 | "type": "string", 47 | "title": "Function name schema.", 48 | "description": "Function name as constructor.", 49 | "name": "functionName" 50 | }, 51 | "schema": { 52 | "id": "http://www.wineryjs.org/object-type-config/item/schema", 53 | "type": "string", 54 | "title": "JSON Schema for value element.", 55 | "description": "JSON schema for value element.", 56 | "name": "schema" 57 | }, 58 | "exampleObjects": { 59 | "id": "http://www.wineryjs.org/provider-config/item/exampleObjects", 60 | "type": "array", 61 | "title": "Example objects schema.", 62 | "description": "Example objects for current type.", 63 | "name": "exampleObjects", 64 | "items": {} 65 | } 66 | }, 67 | "additionalProperties": false, 68 | "required": [ 69 | "typeName", 70 | "description", 71 | "moduleName", 72 | "functionName", 73 | "exampleObjects" 74 | ] 75 | } 76 | } -------------------------------------------------------------------------------- /schema/request-template.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/request-template", 4 | "type": "object", 5 | "title": "Schema for Winery application request.", 6 | "description": "Schema for Winery application request.", 7 | "name": "/", 8 | "properties": { 9 | "base": { 10 | "id": "http://www.wineryjs.org/request-template/base", 11 | "type": "string", 12 | "title": "Base template uri schema.", 13 | "description": "Base template Uri. Case insensitive.", 14 | "name": "base" 15 | }, 16 | "application": { 17 | "id": "http://www.wineryjs.org/request-template/application", 18 | "type": "string", 19 | "title": "Application name schema.", 20 | "description": "Application name. Case insensitive.", 21 | "name": "application" 22 | }, 23 | "overrideTypes": { 24 | "id": "http://www.wineryjs.org/request-template/overrideTypes", 25 | "type": "array", 26 | "title": "Override types.", 27 | "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.", 28 | "name": "overrideTypes", 29 | "items": { 30 | "id": "http://www.wineryjs.org/request-template/overrideTypes/item", 31 | "type": "object", 32 | "title": "Override type definition", 33 | "description": "Override type definition", 34 | "name": "0", 35 | "properties": { 36 | "typeName": { 37 | "id": "http://www.wineryjs.org/request-template/overrideTypes/item/typeName", 38 | "type": "string", 39 | "title": "Type name.", 40 | "description": "Type name to override.", 41 | "name": "typeName" 42 | }, 43 | "description": { 44 | "id": "http://www.wineryjs.org/request-template/overrideTypes/item/description", 45 | "type": "string", 46 | "title": "Description.", 47 | "description": "Description for the type. Optional.", 48 | "name": "description" 49 | }, 50 | "moduleName": { 51 | "id": "http://www.wineryjs.org/request-template/overrideTypes/item/moduleName", 52 | "type": "string", 53 | "title": "Module name.", 54 | "description": "Module name for the overridden type.", 55 | "name": "moduleName" 56 | }, 57 | "functionName": { 58 | "id": "http://www.wineryjs.org/request-template/overrideTypes/item/functionName", 59 | "type": "string", 60 | "title": "Function name used as constructor.", 61 | "description": "Function name within the module for the overridden type.", 62 | "name": "functionName" 63 | } 64 | }, 65 | "required": [ "typeName", "moduleName", "functionName" ], 66 | "additionalProperties": false 67 | } 68 | }, 69 | "overrideProviders": { 70 | "id": "http://www.wineryjs.org/request-template/overrideProviders", 71 | "type": "array", 72 | "title": "Override object providers.", 73 | "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.", 74 | "name": "overrideProviders", 75 | "items": { 76 | "id": "http://www.wineryjs.org/request-template/overrideProviders/item", 77 | "type": "object", 78 | "title": "Override provider definition.", 79 | "description": "Definition of an overridden object provider.", 80 | "name": "item", 81 | "properties": { 82 | "protocol": { 83 | "id": "http://www.wineryjs.org/request-template/overrideProviders/item/protocol", 84 | "type": "string", 85 | "title": "Protocol.", 86 | "description": "Protocol of the provider.", 87 | "name": "protocol" 88 | }, 89 | "description": { 90 | "id": "http://www.wineryjs.org/request-template/overrideProviders/item/description", 91 | "type": "string", 92 | "title": "Description.", 93 | "description": "Description of the provider. Optional.", 94 | "name": "description" 95 | }, 96 | "moduleName": { 97 | "id": "http://www.wineryjs.org/request-template/overrideProviders/item/moduleName", 98 | "type": "string", 99 | "title": "Module name", 100 | "description": "Module name of the provider.", 101 | "name": "moduleName" 102 | }, 103 | "functionName": { 104 | "id": "http://www.wineryjs.org/request-template/overrideProviders/item/functionName", 105 | "type": "string", 106 | "title": "Function name.", 107 | "description": "Function name of the provider to load objects.", 108 | "name": "functionName" 109 | } 110 | }, 111 | "required": [ "protocol", "moduleName", "functionName" ], 112 | "additionalProperties": false 113 | } 114 | }, 115 | "overrideObjects": { 116 | "id": "http://www.wineryjs.org/request-template/overrideObjects", 117 | "type": "array", 118 | "title": "Override named objects.", 119 | "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.", 120 | "name": "overrideObjects", 121 | "items": { 122 | "id": "http://www.wineryjs.org/request-template/overrideObjects/item", 123 | "type": "object", 124 | "title": "Override named object definition.", 125 | "description": "Override named object definition.", 126 | "name": "item", 127 | "properties": { 128 | "name": { 129 | "id": "http://www.wineryjs.org/request-template/overrideObjects/item/name", 130 | "type": "string", 131 | "title": "Name.", 132 | "description": "Name of the override object.", 133 | "name": "name" 134 | }, 135 | "description": { 136 | "id": "http://www.wineryjs.org/request-template/overrideObjects/item/description", 137 | "type": "string", 138 | "title": "Description.", 139 | "description": "Description of the named object. Optional.", 140 | "name": "description" 141 | }, 142 | "value": { 143 | "id": "http://www.wineryjs.org/request-template/overrideObjects/item/value", 144 | "title": "Value.", 145 | "description": "Value of override named objects.", 146 | "name": "value" 147 | } 148 | }, 149 | "required": [ "name", "value" ], 150 | "additionalProperties": false 151 | } 152 | } 153 | }, 154 | "additionalProperties": false, 155 | "oneOf": [ 156 | { 157 | "required": ["application"] 158 | }, 159 | { 160 | "required": ["base"] 161 | } 162 | ] 163 | } 164 | -------------------------------------------------------------------------------- /schema/request.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/request", 4 | "type": "object", 5 | "title": "Schema for Winery application request.", 6 | "description": "Schema for Winery application request.", 7 | "name": "/", 8 | "properties": { 9 | "base": { 10 | "id": "http://www.wineryjs.org/request/base", 11 | "type": "string", 12 | "title": "Base template uri schema.", 13 | "description": "Base template Uri. Case insensitive.", 14 | "name": "base" 15 | }, 16 | "application": { 17 | "id": "http://www.wineryjs.org/request/application", 18 | "type": "string", 19 | "title": "Application name schema.", 20 | "description": "Application name. Case insensitive.", 21 | "name": "application" 22 | }, 23 | "entryPoint": { 24 | "id": "http://www.wineryjs.org/request/entryPoint", 25 | "type": "string", 26 | "title": "EntryPoint name schema.", 27 | "description": "Entrypoint name defined as named object. Case sensitive.", 28 | "name": "entryPoint" 29 | }, 30 | "traceId": { 31 | "id": "http://www.wineryjs.org/request/traceId", 32 | "type": "string", 33 | "title": "TraceId string.", 34 | "description": "Trace ID used for debugging purpose.", 35 | "name": "traceId" 36 | }, 37 | "input": { 38 | "id": "http://www.wineryjs.org/request/input", 39 | "title": "Entry point input.", 40 | "description": "Input JS value to entry point.", 41 | "name": "input" 42 | }, 43 | "controlFlags": { 44 | "id": "http://www.wineryjs.org/request/controlFlags", 45 | "type": "object", 46 | "title": "Control flags.", 47 | "description": "Control flags for current request.", 48 | "name": "controlFlags", 49 | "properties": { 50 | "debug": { 51 | "id": "http://www.wineryjs.org/request/controlFlags/debug", 52 | "type": "boolean", 53 | "title": "Debug flag.", 54 | "description": "Enable debug or not.", 55 | "name": "debug", 56 | "default": false 57 | }, 58 | "perf": { 59 | "id": "http://www.wineryjs.org/request/controlFlags/perf", 60 | "type": "boolean", 61 | "title": "Performance measure flag.", 62 | "description": "Enable performance measure or not.", 63 | "name": "perf", 64 | "default": false 65 | } 66 | }, 67 | "additionalProperties": false 68 | }, 69 | "overrideTypes": { 70 | "id": "http://www.wineryjs.org/request/overrideTypes", 71 | "type": "array", 72 | "title": "Override types.", 73 | "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.", 74 | "name": "overrideTypes", 75 | "items": { 76 | "id": "http://www.wineryjs.org/request/overrideTypes/item", 77 | "type": "object", 78 | "title": "Override type definition", 79 | "description": "Override type definition", 80 | "name": "0", 81 | "properties": { 82 | "typeName": { 83 | "id": "http://www.wineryjs.org/request/overrideTypes/item/typeName", 84 | "type": "string", 85 | "title": "Type name.", 86 | "description": "Type name to override.", 87 | "name": "typeName" 88 | }, 89 | "description": { 90 | "id": "http://www.wineryjs.org/request/overrideTypes/item/description", 91 | "type": "string", 92 | "title": "Description.", 93 | "description": "Description for the type. Optional.", 94 | "name": "description" 95 | }, 96 | "moduleName": { 97 | "id": "http://www.wineryjs.org/request/overrideTypes/item/moduleName", 98 | "type": "string", 99 | "title": "Module name.", 100 | "description": "Module name for the overridden type.", 101 | "name": "moduleName" 102 | }, 103 | "functionName": { 104 | "id": "http://www.wineryjs.org/request/overrideTypes/item/functionName", 105 | "type": "string", 106 | "title": "Function name used as constructor.", 107 | "description": "Function name within the module for the overridden type.", 108 | "name": "functionName" 109 | } 110 | }, 111 | "required": [ "typeName", "moduleName", "functionName" ], 112 | "additionalProperties": false 113 | } 114 | }, 115 | "overrideProviders": { 116 | "id": "http://www.wineryjs.org/request/overrideProviders", 117 | "type": "array", 118 | "title": "Override object providers.", 119 | "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.", 120 | "name": "overrideProviders", 121 | "items": { 122 | "id": "http://www.wineryjs.org/request/overrideProviders/item", 123 | "type": "object", 124 | "title": "Override provider definition.", 125 | "description": "Definition of an overridden object provider.", 126 | "name": "item", 127 | "properties": { 128 | "protocol": { 129 | "id": "http://www.wineryjs.org/request/overrideProviders/item/protocol", 130 | "type": "string", 131 | "title": "Protocol.", 132 | "description": "Protocol of the provider.", 133 | "name": "protocol" 134 | }, 135 | "description": { 136 | "id": "http://www.wineryjs.org/request/overrideProviders/item/description", 137 | "type": "string", 138 | "title": "Description.", 139 | "description": "Description of the provider. Optional.", 140 | "name": "description" 141 | }, 142 | "moduleName": { 143 | "id": "http://www.wineryjs.org/request/overrideProviders/item/moduleName", 144 | "type": "string", 145 | "title": "Module name", 146 | "description": "Module name of the provider.", 147 | "name": "moduleName" 148 | }, 149 | "functionName": { 150 | "id": "http://www.wineryjs.org/request/overrideProviders/item/functionName", 151 | "type": "string", 152 | "title": "Function name.", 153 | "description": "Function name of the provider to load objects.", 154 | "name": "functionName" 155 | } 156 | }, 157 | "required": [ "protocol", "moduleName", "functionName" ], 158 | "additionalProperties": false 159 | } 160 | }, 161 | "overrideObjects": { 162 | "id": "http://www.wineryjs.org/request/overrideObjects", 163 | "type": "array", 164 | "title": "Override named objects.", 165 | "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.", 166 | "name": "overrideObjects", 167 | "items": { 168 | "id": "http://www.wineryjs.org/request/overrideObjects/item", 169 | "type": "object", 170 | "title": "Override named object definition.", 171 | "description": "Override named object definition.", 172 | "name": "item", 173 | "properties": { 174 | "name": { 175 | "id": "http://www.wineryjs.org/request/overrideObjects/item/name", 176 | "type": "string", 177 | "title": "Name.", 178 | "description": "Name of the override object.", 179 | "name": "name" 180 | }, 181 | "description": { 182 | "id": "http://www.wineryjs.org/request/overrideObjects/item/description", 183 | "type": "string", 184 | "title": "Description.", 185 | "description": "Description of the named object. Optional.", 186 | "name": "description" 187 | }, 188 | "value": { 189 | "id": "http://www.wineryjs.org/request/overrideObjects/item/value", 190 | "title": "Value.", 191 | "description": "Value of override named objects.", 192 | "name": "value" 193 | } 194 | }, 195 | "required": [ "name", "value" ], 196 | "additionalProperties": false 197 | } 198 | } 199 | }, 200 | "additionalProperties": false, 201 | "required": ["entryPoint"], 202 | "oneOf": [ 203 | { 204 | "required": ["application"] 205 | }, 206 | { 207 | "required": ["base"] 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /schema/response.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://www.wineryjs.org/response", 4 | "type": "object", 5 | "title": "Winery response schema.", 6 | "description": "Winery Response schema.", 7 | "name": "/", 8 | "properties": { 9 | "responseCode": { 10 | "id": "http://www.wineryjs.org/response/responseCode", 11 | "type": "integer", 12 | "title": "ResponseCode schema.", 13 | "description": "Response code.", 14 | "name": "responseCode" 15 | }, 16 | "errorMessage": { 17 | "id": "http://www.wineryjs.org/response/errorMessage", 18 | "type": "string", 19 | "title": "ErrorMessage schema.", 20 | "description": "Error message if request failed.", 21 | "name": "errorMessage" 22 | }, 23 | "output": { 24 | "id": "http://www.wineryjs.org/response/output", 25 | "title": "Output schema.", 26 | "description": "Output object from entrypoint.", 27 | "name": "output" 28 | }, 29 | "debugInfo": { 30 | "id": "http://www.wineryjs.org/response/debugInfo", 31 | "type": "object", 32 | "title": "DebugInfo schema.", 33 | "description": "Debug information when debug flag is on in request.", 34 | "name": "debugInfo", 35 | "properties": { 36 | "exception": { 37 | "id": "http://www.wineryjs.org/response/debugInfo/exception", 38 | "type": "object", 39 | "title": "Exception schema.", 40 | "description": "Exception information from response.", 41 | "name": "exception", 42 | "properties": { 43 | "stack": { 44 | "id": "http://www.wineryjs.org/response/debugInfo/exception/stack", 45 | "type": "string", 46 | "title": "Stack schema.", 47 | "description": "Stack trace of the exception.", 48 | "name": "stack" 49 | }, 50 | "message": { 51 | "id": "http://www.wineryjs.org/response/debugInfo/exception/message", 52 | "type": "string", 53 | "title": "Message schema.", 54 | "description": "Message of the exception.", 55 | "name": "message" 56 | }, 57 | "fileName": { 58 | "id": "http://www.wineryjs.org/response/debugInfo/exception/fileName", 59 | "type": "string", 60 | "title": "FileName schema.", 61 | "description": "File name from where the exception is thrown.", 62 | "name": "fileName" 63 | }, 64 | "lineNumber": { 65 | "id": "http://www.wineryjs.org/response/debugInfo/exception/lineNumber", 66 | "type": "integer", 67 | "title": "LineNumber schema.", 68 | "description": "Line number from where the exception is thrown.", 69 | "name": "lineNumber" 70 | }, 71 | "columnNumber": { 72 | "id": "http://www.wineryjs.org/response/debugInfo/exception/columnNumber", 73 | "type": "integer", 74 | "title": "ColumnNumber schema.", 75 | "description": "Column number from where the exception is thrown.", 76 | "name": "columnNumber" 77 | } 78 | }, 79 | "additionalProperties": false 80 | }, 81 | "events": { 82 | "id": "http://www.wineryjs.org/response/debugInfo/events", 83 | "type": "array", 84 | "title": "Events schema.", 85 | "description": "Debug events generated from logger.", 86 | "name": "events", 87 | "items": { 88 | "id": "http://www.wineryjs.org/response/debugInfo/events/item", 89 | "type": "object", 90 | "title": "Item schema.", 91 | "description": "An explanation about the puropose of this instance described by this schema.", 92 | "name": "item", 93 | "properties": { 94 | "time": { 95 | "id": "http://www.wineryjs.org/response/debugInfo/events/item/time", 96 | "type": "string", 97 | "title": "Time schema.", 98 | "description": "Time of the debugging event.", 99 | "name": "time" 100 | }, 101 | "logLevel": { 102 | "id": "http://www.wineryjs.org/response/debugInfo/events/item/logLevel", 103 | "type": "string", 104 | "title": "LogLevel schema.", 105 | "description": "Log level of the event.", 106 | "name": "logLevel" 107 | }, 108 | "message": { 109 | "id": "http://www.wineryjs.org/response/debugInfo/events/0/message", 110 | "type": "string", 111 | "title": "Message schema.", 112 | "description": "Message of the event.", 113 | "name": "message" 114 | } 115 | }, 116 | "additionalProperties": false 117 | } 118 | }, 119 | "details": { 120 | "id": "http://www.wineryjs.org/response/debugInfo/details", 121 | "type": "object", 122 | "title": "Details schema.", 123 | "description": "Details in key/values.", 124 | "name": "details", 125 | "properties": {}, 126 | "additionalProperties": true 127 | } 128 | }, 129 | "additionalProperties": false 130 | }, 131 | "perfInfo": { 132 | "id": "http://www.wineryjs.org/response/perfInfo", 133 | "type": "object", 134 | "title": "PerfInfo schema.", 135 | "description": "Performance numbers when perf flag is on at request.", 136 | "name": "perfInfo", 137 | "additionalProperties": true 138 | } 139 | }, 140 | "additionalProperties": false, 141 | "required": [ 142 | "responseCode" 143 | ] 144 | } 145 | -------------------------------------------------------------------------------- /test/application-test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from 'assert'; 5 | import * as path from 'path'; 6 | import { Application, ApplicationSettings, ApplicationConfig } from '../lib/application'; 7 | import * as builtins from '../lib/builtins'; 8 | import { HostConfig, Leaf } from '../lib/host'; 9 | 10 | describe('winery/application', () => { 11 | let host = new Leaf( 12 | HostConfig.fromConfig( 13 | require.resolve('../config/host.json'))); 14 | 15 | let appSettings: ApplicationSettings = undefined; 16 | let app: Application = undefined; 17 | 18 | describe('ApplicationConfig', () => { 19 | it('#fromConfig', () => { 20 | appSettings = ApplicationConfig.fromConfig(host.settings, 21 | path.resolve(__dirname, "test-app/app.json")); 22 | }); 23 | 24 | it('#getters', () => { 25 | assert.equal(appSettings.metrics.length, 1); 26 | }) 27 | }); 28 | 29 | describe('Application', () => { 30 | it('#ctor', () => { 31 | app = new Application(host.objectContext, 32 | ApplicationConfig.fromConfig( 33 | host.settings, 34 | path.resolve(__dirname, "test-app/app.json"))); 35 | }); 36 | 37 | it('#getters', () => { 38 | assert.equal(app.id, 'test-app'); 39 | assert.equal(Object.keys(app.metrics).length, 1); 40 | }); 41 | 42 | it('#create', () => { 43 | assert.equal(app.create({ 44 | _type: "TypeA", 45 | value: "abc" 46 | }), "A:abc"); 47 | assert.strictEqual(app.create("protocolA:/abc"), "A:abc"); 48 | }); 49 | 50 | it('#get', () => { 51 | assert.equal(app.get('object1'), "A:abc"); 52 | }); 53 | 54 | it('#getEntryPoint', () => { 55 | assert.strictEqual(app.getEntryPoint("listEntryPoints"), builtins.entryPoints.listEntryPoints); 56 | }) 57 | 58 | it('#getInterceptor', () => { 59 | assert.strictEqual(app.getInterceptor("executeEntryPoint"), builtins.interceptors.executeEntryPoint); 60 | }); 61 | 62 | it('#getExecutionStack', () => { 63 | let stack = app.getExecutionStack('foo'); 64 | assert.equal(stack.length, 2); 65 | assert.strictEqual(stack[0], builtins.interceptors.finalizeResponse); 66 | assert.strictEqual(stack[1], builtins.interceptors.executeEntryPoint); 67 | 68 | stack = app.getExecutionStack('bar'); 69 | assert.equal(stack.length, 3); 70 | assert.strictEqual(stack[0], builtins.interceptors.logRequestResponse); 71 | assert.strictEqual(stack[1], builtins.interceptors.finalizeResponse); 72 | assert.strictEqual(stack[2], builtins.interceptors.executeEntryPoint); 73 | }); 74 | }); 75 | }); 76 | 77 | -------------------------------------------------------------------------------- /test/config/utils-test.json: -------------------------------------------------------------------------------- 1 | // This JSON file follows the schema ./test-json-schema.json. 2 | { 3 | "stringProp": "hi", 4 | "numberProp": 0, 5 | "booleanProp": true, 6 | "arrayProp": [ 7 | 1, 8 | 2 9 | ], 10 | // objectProp can have additional properties. 11 | "objectProp": { 12 | "field1": 1, 13 | "additionalField": "additional" 14 | } 15 | } -------------------------------------------------------------------------------- /test/config/utils-test.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "additionalProperties": false, 4 | "id": "http://example.com/root.json", 5 | "properties": { 6 | "arrayProp": { 7 | "id": "arrayProp", 8 | "items": { 9 | "default": 1, 10 | "description": "An explanation about the purpose of this instance.", 11 | "id": "0", 12 | "title": "The 0 schema.", 13 | "type": "integer" 14 | }, 15 | "type": "array" 16 | }, 17 | "booleanProp": { 18 | "default": true, 19 | "description": "An explanation about the purpose of this instance.", 20 | "id": "booleanProp", 21 | "title": "The Booleanprop schema.", 22 | "type": "boolean" 23 | }, 24 | "numberProp": { 25 | "default": 0, 26 | "description": "An explanation about the purpose of this instance.", 27 | "id": "numberProp", 28 | "title": "The Numberprop schema.", 29 | "type": "integer" 30 | }, 31 | "objectProp": { 32 | "additionalProperties": true, 33 | "id": "objectProp", 34 | "properties": { 35 | "field1": { 36 | "default": 1, 37 | "description": "An explanation about the purpose of this instance.", 38 | "id": "field1", 39 | "title": "The Field1 schema.", 40 | "type": "integer" 41 | } 42 | }, 43 | "required": [ 44 | "field1" 45 | ], 46 | "type": "object" 47 | }, 48 | "stringProp": { 49 | "default": "hi", 50 | "description": "An explanation about the purpose of this instance.", 51 | "id": "stringProp", 52 | "title": "The Stringprop schema.", 53 | "type": "string" 54 | } 55 | }, 56 | "required": [ 57 | "booleanProp", 58 | "arrayProp", 59 | "objectProp", 60 | "stringProp", 61 | "numberProp" 62 | ], 63 | "type": "object" 64 | } -------------------------------------------------------------------------------- /test/config/utils-test.xml: -------------------------------------------------------------------------------- 1 | 2 | hi 3 | 0 4 | true 5 | 6 | 1 7 | 2 8 | 9 | 10 | 1 11 | 12 | -------------------------------------------------------------------------------- /test/metric-test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from 'assert'; 5 | import * as path from 'path'; 6 | import { metric } from 'napajs'; 7 | import { MetricConfig } from '../lib/metric'; 8 | 9 | describe('winery/metric', () => { 10 | describe('MetricConfig', () => { 11 | it('#fromConfigObject: good config', () => { 12 | let configObject = [ 13 | { 14 | name: "myCounter1", 15 | displayName: "My counter1", 16 | description: "Counter description", 17 | type: "Percentile", 18 | dimensionNames: ["d1", "d2"] 19 | }, 20 | { 21 | name: "myCounter2", 22 | displayName: "My counter2", 23 | description: "Counter description", 24 | type: "Rate", 25 | dimensionNames: [] 26 | }, 27 | { 28 | name: "myCounter3", 29 | displayName: "My counter3", 30 | description: "Counter description", 31 | type: "Number", 32 | dimensionNames: [] 33 | } 34 | ] 35 | let defs = MetricConfig.fromConfigObject("DefaultSection", configObject); 36 | assert.deepEqual(defs, [ 37 | { 38 | name: "myCounter1", 39 | sectionName: "DefaultSection", 40 | displayName: "My counter1", 41 | description: "Counter description", 42 | type: metric.MetricType.Percentile, 43 | dimensionNames: ["d1", "d2"] 44 | }, 45 | { 46 | name: "myCounter2", 47 | sectionName: "DefaultSection", 48 | displayName: "My counter2", 49 | description: "Counter description", 50 | type: metric.MetricType.Rate, 51 | dimensionNames: [] 52 | }, 53 | { 54 | name: "myCounter3", 55 | sectionName: "DefaultSection", 56 | displayName: "My counter3", 57 | description: "Counter description", 58 | type: metric.MetricType.Number, 59 | dimensionNames: [] 60 | } 61 | ]) 62 | }); 63 | 64 | it ('#fromConfig', () => { 65 | assert.doesNotThrow(() => { 66 | MetricConfig.fromConfig("DefaultSection", 67 | path.resolve(__dirname, "test-app/metrics.json")); 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/named-object-test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from 'assert'; 5 | import * as path from 'path'; 6 | 7 | import { NamedObject, NamedObjectConfig, NamedObjectRegistry } from '../lib/named-object'; 8 | 9 | describe('winery/named-object', () => { 10 | describe('NamedObjectConfig', () => { 11 | it('#fromConfigObject: good config', () => { 12 | let configObject = [ 13 | { 14 | name: "object1", 15 | value: { 16 | _type: "TypeA", 17 | value: 1 18 | } 19 | }, 20 | { 21 | name: "object2", 22 | value: 1 23 | } 24 | ] 25 | 26 | let defs = NamedObjectConfig.fromConfigObject(configObject, true); 27 | assert.deepEqual(defs, [ 28 | { 29 | name: "object1", 30 | value: { 31 | _type: "TypeA", 32 | value: 1 33 | }, 34 | // Set default values. 35 | override: false, 36 | private: false 37 | }, 38 | { 39 | name: "object2", 40 | value: 1, 41 | override: false, 42 | private: false 43 | } 44 | ]) 45 | }); 46 | 47 | it('#fromConfigObject: not conform with schema', () => { 48 | let configObject = [ 49 | { 50 | name: "object1", 51 | // Should be value. 52 | valueDef: 1 53 | } 54 | ] 55 | assert.throws( () => { 56 | NamedObjectConfig.fromConfigObject(configObject, true); 57 | }); 58 | }); 59 | 60 | it ('#fromConfig', () => { 61 | assert.doesNotThrow(() => { 62 | NamedObjectConfig.fromConfig( 63 | path.resolve(__dirname, "test-app/objects.json")); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('NamedObjectRegistry', () => { 69 | let collection = new NamedObjectRegistry(); 70 | let objectA: NamedObject = { 71 | scope: "global", 72 | def: { 73 | name: "objectA", 74 | value: 1 75 | }, 76 | value: 1 77 | } 78 | 79 | it('#has', () => { 80 | assert(!collection.has('objectA')); 81 | }); 82 | 83 | it('#insert', () => { 84 | collection.insert(objectA); 85 | assert(collection.has('objectA')); 86 | }); 87 | 88 | it('#get', () => { 89 | let output = collection.get("objectA") 90 | assert.strictEqual(output, objectA); 91 | }); 92 | 93 | it('#forEach', () => { 94 | collection.forEach((object: NamedObject) => { 95 | assert.strictEqual(object, objectA); 96 | }); 97 | }) 98 | 99 | it('#fromDefinition', () => { 100 | let objectContext = { 101 | create: (input: any): any => { 102 | return input; 103 | }, 104 | get: (name: string): NamedObject => { 105 | return null; 106 | }, 107 | forEach: (callback: (object: NamedObject) => void) => { 108 | // Do nothing. 109 | }, 110 | baseDir: __dirname 111 | } 112 | collection = NamedObjectRegistry.fromDefinition( 113 | "global", 114 | [{ name: "objectA", value: 1 }], 115 | objectContext); 116 | assert(collection.has('objectA')); 117 | assert.deepEqual(collection.get('objectA'), objectA); 118 | }) 119 | }); 120 | }); -------------------------------------------------------------------------------- /test/object-context-test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from 'assert'; 5 | import { 6 | ScopedObjectContextDef, 7 | ScopedObjectContext, 8 | TypeDef, 9 | ProviderDef, 10 | NamedObjectDef, 11 | NamedObject, 12 | ObjectContext, 13 | Uri 14 | } from '../lib/object-model'; 15 | 16 | describe('winery/object-context', () => { 17 | let perAppContextDef: ScopedObjectContextDef = null; 18 | let perRequestContextDef: ScopedObjectContextDef = null; 19 | 20 | // Test suite for ScopedObjectContextDefinition. 21 | describe('ScopedObjectContextDefinition', () => { 22 | // Per app definitions. 23 | let perAppTypeDefs: TypeDef[] = [ 24 | { 25 | typeName: "TypeA", 26 | moduleName: "./object-context-test", 27 | functionName: "types.createTypeA" 28 | }, 29 | { 30 | typeName: "TypeB", 31 | moduleName: "./object-context-test", 32 | functionName: "types.createTypeB" 33 | } 34 | ]; 35 | 36 | let perAppProviderDefs: ProviderDef[] = [ 37 | { 38 | protocol: "ProtocolA", 39 | moduleName: "./object-context-test", 40 | functionName: "provideProtocolA" 41 | } 42 | ]; 43 | 44 | let perAppObjectDefs: NamedObjectDef[] = [ 45 | { 46 | name: "objectA", 47 | value: { 48 | _type: "TypeA", 49 | value: 1 50 | } 51 | }, 52 | { 53 | name: "objectB", 54 | value: { 55 | _type: "TypeB", 56 | value: { 57 | _type: "TypeA", 58 | value: 1 59 | } 60 | } 61 | }, 62 | { 63 | name: "objectC", 64 | value: "ProtocolA://abc" 65 | }, 66 | ]; 67 | 68 | // Per request definitions. 69 | let perRequestTypeDefs: TypeDef[] = [ 70 | { 71 | typeName: "TypeA", 72 | moduleName: "./object-context-test", 73 | functionName: "types.createTypeAPrime" 74 | } 75 | ]; 76 | 77 | let perRequestProviderDefs: ProviderDef[] = [ 78 | { 79 | protocol: "ProtocolA", 80 | moduleName: "./object-context-test", 81 | functionName: "provideProtocolAPrime" 82 | } 83 | ]; 84 | 85 | let perRequestObjectDefs: NamedObjectDef[] = [ 86 | { 87 | name: "objectA", 88 | value: { 89 | _type: "TypeA", 90 | value: 2 91 | } 92 | }, 93 | { 94 | name: "objectC", 95 | value: "ProtocolA://cde" 96 | }, 97 | { 98 | name: "objectD", 99 | value: "ProtocolA://def" 100 | } 101 | ]; 102 | 103 | it("#ctor", () => { 104 | assert.doesNotThrow(() => { 105 | perAppContextDef = new ScopedObjectContextDef(null, perAppTypeDefs, perAppProviderDefs, perAppObjectDefs, true); 106 | perRequestContextDef = new ScopedObjectContextDef(perAppContextDef, perRequestTypeDefs, perRequestProviderDefs, perRequestObjectDefs, false); 107 | }); 108 | }); 109 | 110 | it('#getters', () => { 111 | assert.strictEqual(perAppContextDef.parent, null); 112 | assert.strictEqual(perAppContextDef.typeDefs, perAppTypeDefs); 113 | assert.strictEqual(perAppContextDef.providerDefs, perAppProviderDefs); 114 | assert.strictEqual(perAppContextDef.namedObjectDefs, perAppObjectDefs); 115 | 116 | assert.strictEqual(perAppContextDef.getTypeDef('TypeA'), perAppTypeDefs[0]); 117 | assert.strictEqual(perAppContextDef.getTypeDef('TypeB'), perAppTypeDefs[1]); 118 | assert.strictEqual(perAppContextDef.getProviderDef('ProtocolA'), perAppProviderDefs[0]); 119 | 120 | assert.strictEqual(perAppContextDef.getNamedObjectDef('objectA'), perAppObjectDefs[0]); 121 | assert.strictEqual(perAppContextDef.getNamedObjectDef('objectB'), perAppObjectDefs[1]); 122 | assert.strictEqual(perAppContextDef.getNamedObjectDef('objectC'), perAppObjectDefs[2]); 123 | }); 124 | 125 | it('#analyzeDependency', () => { 126 | // objectA 127 | let dep1 = perAppObjectDefs[0].dependencies; 128 | assert(dep1.objectDependencies.size == 0); 129 | assert(dep1.protocolDependencies.size == 0); 130 | assert(dep1.typeDependencies.size == 1 && dep1.typeDependencies.has('TypeA')); 131 | 132 | // objectB 133 | let dep2 = perAppObjectDefs[1].dependencies; 134 | assert(dep2.objectDependencies.size == 0); 135 | assert(dep2.protocolDependencies.size == 0); 136 | assert(dep2.typeDependencies.size == 2 137 | && dep2.typeDependencies.has('TypeA') 138 | && dep2.typeDependencies.has('TypeB')); 139 | 140 | // objectC 141 | let dep3 = perAppObjectDefs[2].dependencies; 142 | assert(dep3.objectDependencies.size == 0); 143 | assert(dep3.protocolDependencies.size == 1 && dep3.protocolDependencies.has('ProtocolA')); 144 | assert(dep3.typeDependencies.size == 0); 145 | }); 146 | }); 147 | 148 | // Test suite for ScopedObjectContext 149 | describe('ScopedObjectContext', () => { 150 | let perAppContext: ScopedObjectContext = null; 151 | let perRequestContext: ScopedObjectContext = null; 152 | 153 | it('#ctor', () => { 154 | perAppContext = new ScopedObjectContext("./application", __dirname, null, perAppContextDef); 155 | perRequestContext = new ScopedObjectContext("request", __dirname, perAppContext, perRequestContextDef); 156 | }); 157 | 158 | it('#getters', () => { 159 | assert.strictEqual(perAppContext.scope, "./application"); 160 | assert.strictEqual(perAppContext.baseDir, __dirname); 161 | assert.strictEqual(perAppContext.def, perAppContextDef); 162 | assert.strictEqual(perAppContext.parent, null); 163 | assert.strictEqual(perRequestContext.parent, perAppContext); 164 | }); 165 | 166 | it('#create: overridden TypeA', () => { 167 | let inputA = { _type: "TypeA", value: 1}; 168 | assert.strictEqual(perAppContext.create(inputA), 1); 169 | assert.strictEqual(perRequestContext.create(inputA), 2); 170 | }); 171 | 172 | it('#create: not overridden TypeB', () => { 173 | let inputB = { _type: "TypeB", value: { _type: "TypeA", value: 1}}; 174 | assert.strictEqual(perAppContext.create(inputB), 1); 175 | // B returns A's value, which is different from per-app and per-request. 176 | assert.strictEqual(perRequestContext.create(inputB), 2); 177 | }); 178 | 179 | it('#create: overridden ProtocolA', () => { 180 | let uri = "ProtocolA://abc"; 181 | assert.strictEqual(perAppContext.create(uri), "/abc"); 182 | assert.strictEqual(perRequestContext.create(uri), "/abc*"); 183 | }); 184 | 185 | it('#get: overriden objectA', () => { 186 | let objectA = perAppContext.get('objectA'); 187 | assert.strictEqual(objectA.scope, './application'); 188 | assert.strictEqual(objectA.value, 1); 189 | 190 | objectA = perRequestContext.get('objectA'); 191 | assert.strictEqual(objectA.scope, 'request'); 192 | assert.strictEqual(objectA.value, 3); 193 | }); 194 | 195 | it('#get: not overridden objectB but depenent types has been overridden', () => { 196 | let objectB = perAppContext.get('objectB'); 197 | assert.strictEqual(objectB.scope, './application'); 198 | assert.strictEqual(objectB.value, 1); 199 | 200 | /// object B 201 | objectB = perRequestContext.get('objectB'); 202 | assert.strictEqual(objectB.scope, 'request'); 203 | assert.strictEqual(objectB.value, 2); 204 | }); 205 | 206 | it('#get: overriden objectC with new providerA', () => { 207 | let objectC = perAppContext.get('objectC'); 208 | assert.strictEqual(objectC.scope, './application'); 209 | assert.strictEqual(objectC.value, '/abc'); 210 | 211 | objectC = perRequestContext.get('objectC'); 212 | assert.strictEqual(objectC.scope, 'request'); 213 | assert.strictEqual(objectC.value, '/cde*'); 214 | }); 215 | 216 | it('#get: new objectD with new providerA', () => { 217 | let objectD = perAppContext.get('objectD'); 218 | assert(objectD == null); 219 | 220 | objectD = perRequestContext.get('objectD'); 221 | assert.strictEqual(objectD.scope, 'request'); 222 | assert.strictEqual(objectD.value, '/def*'); 223 | }); 224 | 225 | it('#forEach: without parent scope', () => { 226 | let objectNames: string[] = [] 227 | perAppContext.forEach(object => { 228 | objectNames.push(object.def.name); 229 | }); 230 | assert.strictEqual(objectNames.length, 3); 231 | assert(objectNames.indexOf('objectA') >= 0); 232 | assert(objectNames.indexOf('objectB') >= 0); 233 | assert(objectNames.indexOf('objectC') >= 0); 234 | }); 235 | 236 | it('#forEach: with parent scope', () => { 237 | let objectCount = 0; 238 | let objectByName = new Map(); 239 | perRequestContext.forEach(object => { 240 | ++objectCount; 241 | objectByName.set(object.def.name, object); 242 | if (object.def.name !== 'objectB') { 243 | assert(object.scope === 'request'); 244 | } 245 | }); 246 | assert.strictEqual(objectCount, 4); 247 | assert.strictEqual(objectByName.size, objectCount); 248 | assert(objectByName.has('objectA') && objectByName.get('objectA').scope === 'request'); 249 | assert(objectByName.has('objectB') && objectByName.get('objectB').scope === 'request'); 250 | assert(objectByName.has('objectC') && objectByName.get('objectC').scope === 'request'); 251 | assert(objectByName.has('objectD') && objectByName.get('objectD').scope === 'request'); 252 | }); 253 | 254 | // TODO: Add test for needsUpdate. 255 | it('#needsUpdate'); 256 | }); 257 | }); 258 | 259 | export type TypeAInput = { _type: "TypeA", value: number}; 260 | 261 | // Test calling function with 262 | export namespace types { 263 | export function createTypeA(input: TypeAInput | TypeAInput[]) { 264 | if (Array.isArray(input)) { 265 | return input.map(elem => elem.value); 266 | } 267 | return input.value; 268 | } 269 | 270 | export function createTypeAPrime(input: TypeAInput | TypeAInput[]) { 271 | if (Array.isArray(input)) { 272 | return input.map(elem => elem.value + 1); 273 | } 274 | return input.value + 1; 275 | } 276 | 277 | export function createTypeB(input: TypeBInput, context: ObjectContext) { 278 | return context.create(input.value); 279 | } 280 | } 281 | 282 | export type TypeBInput = { _type: "TypeB", value: TypeAInput}; 283 | 284 | export function provideProtocolA(uri: Uri): any { 285 | return uri.path; 286 | } 287 | 288 | export function provideProtocolAPrime(uri: Uri): any { 289 | return uri.path + '*'; 290 | } -------------------------------------------------------------------------------- /test/object-provider-test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from 'assert'; 5 | import * as path from 'path'; 6 | 7 | import { 8 | NamedObject, 9 | ObjectContext, 10 | ProviderDef, 11 | ProviderRegistry, 12 | ProviderConfig, 13 | Uri 14 | } from '../lib/object-model'; 15 | 16 | describe('winery/object-provider', () => { 17 | describe('Uri', () => { 18 | it('#parse: absolute path with no parameters.', () => { 19 | let uri = Uri.parse("doc://a/d/e/f"); 20 | assert.equal(uri.path, "/a/d/e/f"); 21 | assert.equal(uri.protocol, "doc"); 22 | }); 23 | 24 | it('#parse: relative path with no parameters.', () => { 25 | let uri = Uri.parse("doc:/a/d/e/f"); 26 | assert.equal(uri.path, "a/d/e/f"); 27 | assert.equal(uri.protocol, "doc"); 28 | }); 29 | 30 | it('#parse: absolute path with parameters.', () => { 31 | let uri = Uri.parse("doc://a/d/e/f?a=1&b=2"); 32 | assert.equal(uri.path, "/a/d/e/f"); 33 | assert.equal(uri.protocol, "doc"); 34 | assert.strictEqual(uri.getParameter("a"), "1"); 35 | assert.strictEqual(uri.getParameter("b"), "2"); 36 | }); 37 | 38 | it('#parse: bad format.', () => { 39 | assert.throws(() => { 40 | Uri.parse("doc//a/d/e/f?a=1&b=2"); 41 | }); 42 | }); 43 | }); 44 | 45 | 46 | describe('ObjectProviderConfig', () => { 47 | it('#fromConfigObject: good config', () => { 48 | let configObject = [ 49 | { 50 | protocol: "protocolA", 51 | description: "Protocol A", 52 | moduleName: "module", 53 | functionName: "function", 54 | exampleUri: ["protocolA://abc"] 55 | 56 | } 57 | ] 58 | let defs = ProviderConfig.fromConfigObject(configObject, true); 59 | assert.deepEqual(defs, [ 60 | { 61 | protocol: "protocolA", 62 | description: "Protocol A", 63 | moduleName: "module", 64 | functionName: "function", 65 | exampleUri: ["protocolA://abc"], 66 | // Set default property. 67 | override: false 68 | } 69 | ]) 70 | }); 71 | 72 | it('#fromConfigObject: not conform with schema', () => { 73 | let configObject = [ 74 | { 75 | protocol: "protocolA", 76 | // Should be moduleName, and missing exampleUri. 77 | module: "module", 78 | functionName: "function" 79 | } 80 | ] 81 | assert.throws( () => { 82 | ProviderConfig.fromConfigObject(configObject, true); 83 | }); 84 | }); 85 | 86 | it ('#fromConfig', () => { 87 | assert.doesNotThrow(() => { 88 | ProviderConfig.fromConfig( 89 | path.resolve(__dirname, "test-app/object-providers.json")); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('ProviderRegistry', () => { 95 | let provider = new ProviderRegistry(); 96 | 97 | // ProtocolA support both a single element and an array as input. 98 | it('#register', () => { 99 | provider.register('protocolA', 100 | (uri: Uri | Uri[]): string | string[] => { 101 | if (Array.isArray(uri)) { 102 | return uri.map(value => { return value.path; }); 103 | } 104 | return uri.path; 105 | }); 106 | 107 | // ProtocolB needs an ObjectContext to create inner object. 108 | provider.register('protocolB', 109 | (input: Uri, context: ObjectContext): any => { 110 | return path.resolve(context.baseDir, input.path); 111 | }); 112 | }); 113 | 114 | it('#supports', () => { 115 | // Case insensitive. 116 | assert(provider.supports('protocolA')); 117 | assert(provider.supports('ProtocolA')); 118 | assert(provider.supports('protocola')); 119 | 120 | assert(provider.supports('protocolB')); 121 | assert(!provider.supports('protocolC')); 122 | }); 123 | 124 | it('#provide: unsupported protocol', () => { 125 | // Create object of unsupported type. 126 | assert.throws(() => { 127 | provider.provide(Uri.parse("protocolC://abc")); 128 | }, 129 | Error); 130 | }); 131 | 132 | let uriA1 = Uri.parse("protocolA://abc"); 133 | let expectedA1 = "/abc"; 134 | it('#provide: input with single uri', () => { 135 | // Create object with a single uri. 136 | let a1 = provider.provide(uriA1); 137 | assert.strictEqual(a1, expectedA1); 138 | }); 139 | 140 | it('#provide: case insensitive protocol', () => { 141 | // Create object with a single uri. 142 | let a1 = provider.provide(Uri.parse("PrOtOcOlA://abc")); 143 | assert.strictEqual(a1, expectedA1); 144 | }); 145 | 146 | it('#provide: input with array of uri.', () => { 147 | // Create an array of objects with an array of uris. 148 | let uriA2 = Uri.parse("protocolA://cde"); 149 | let arrayA = provider.provide([uriA1, uriA2]); 150 | assert.deepEqual(arrayA, ["/abc", "/cde"]); 151 | }); 152 | 153 | // Create an object that needs ObjectContext. 154 | // Create a simple context. 155 | var context: ObjectContext = { 156 | create: (input: any): any => { 157 | return null; 158 | }, 159 | get: (name: string): NamedObject => { 160 | return null; 161 | }, 162 | forEach: (callback: (object: NamedObject) => void) => { 163 | // Do nothing. 164 | }, 165 | baseDir: __dirname 166 | } 167 | 168 | let uriB1 = Uri.parse("protocolB:/file1.txt"); 169 | it('#provide: protocol needs object context', () => { 170 | assert.equal(provider.provide(uriB1, context), path.resolve(__dirname, "file1.txt")); 171 | }); 172 | 173 | it('#provide: mixed protocol in a Uri array', () => { 174 | // Create an array of objects of different protocol. 175 | assert.throws(() => { 176 | provider.provide([uriA1, uriB1], context); 177 | }, Error); 178 | }); 179 | 180 | it('ProviderRegistry#fromDefinition', () => { 181 | let defs: ProviderDef[] = [{ 182 | protocol: "protocolA", 183 | moduleName: "./object-provider-test", 184 | functionName: "loadA" 185 | }]; 186 | let provider = ProviderRegistry.fromDefinition(defs, __dirname); 187 | assert(provider.supports('protocolA')); 188 | 189 | let uriA1 = Uri.parse("protocolA://abc") 190 | assert.equal(provider.provide(uriA1), "/abc"); 191 | }); 192 | }); 193 | }); 194 | 195 | export function loadA(uri: Uri): string { 196 | return uri.path; 197 | } -------------------------------------------------------------------------------- /test/object-type-test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from 'assert'; 5 | import * as path from 'path'; 6 | 7 | import { ObjectContext } from '../lib/object-context'; 8 | import { NamedObject } from '../lib/named-object'; 9 | import { ObjectWithType, TypeConfig, TypeDef, TypeRegistry } from '../lib/object-type'; 10 | 11 | describe('winery/object-type', () => { 12 | describe('ObjectTypeConfig', () => { 13 | it('#fromConfigObject: good config', () => { 14 | let configObject = [ 15 | { 16 | typeName: "TypeA", 17 | description: "Type A", 18 | moduleName: "module", 19 | functionName: "function", 20 | exampleObjects: [{ 21 | _type: "TypeA", 22 | value: 1 23 | }] 24 | } 25 | ] 26 | let defs = TypeConfig.fromConfigObject(configObject, true); 27 | assert.deepEqual(defs, [ 28 | { 29 | typeName: "TypeA", 30 | description: "Type A", 31 | moduleName: "module", 32 | functionName: "function", 33 | // Set default property. 34 | override: false, 35 | exampleObjects: [{ 36 | _type: "TypeA", 37 | value: 1 38 | }] 39 | } 40 | ]) 41 | }); 42 | 43 | it('#fromConfigObject: not conform with schema', () => { 44 | let configObject = [ 45 | { 46 | // Should be typeName, missing exampleObjects 47 | type: "TypeA", 48 | moduleName: "module", 49 | functionName: "function" 50 | } 51 | ] 52 | assert.throws(() => { 53 | TypeConfig.fromConfigObject(configObject, true); 54 | }); 55 | }); 56 | 57 | it ('#fromConfig', () => { 58 | assert.doesNotThrow(() => { 59 | TypeConfig.fromConfig( 60 | path.resolve(__dirname, "test-app/object-types.json")); 61 | }); 62 | }); 63 | }); 64 | describe('TypeRegistry', () => { 65 | let factory = new TypeRegistry(); 66 | it('#register', () => { 67 | // TypeA constructor support both a single element and an array as input. 68 | type TypeAInput = { "_type": "TypeA", "value": number}; 69 | factory.register('TypeA', 70 | (input: TypeAInput | TypeAInput[]): number | number[] => { 71 | if (Array.isArray(input)) { 72 | return input.map(value => { return value.value; }); 73 | } 74 | return input.value; 75 | }); 76 | 77 | // TypeB constructor needs an ObjectContext to create inner object. 78 | factory.register('TypeB', 79 | (input: {"_type": "TypeB", "value": ObjectWithType}, context: ObjectContext): any => { 80 | return context.create(input.value); 81 | }); 82 | }); 83 | 84 | it('#supports', () => { 85 | assert(factory.supports('TypeA')); 86 | assert(factory.supports('TypeB')); 87 | assert(!factory.supports('TypeC')); 88 | }); 89 | 90 | it('#create: unsupported type', () => { 91 | // Create object of unsupported type. 92 | assert.throws(() => { 93 | factory.create({'_type': 'TypeC'}) 94 | }, 95 | Error); 96 | }); 97 | 98 | let inputA1 = { "_type": "TypeA", "value": 1}; 99 | let expectedA1 = 1; 100 | it('#create: input as single element', () => { 101 | // Create object with a single element. 102 | let a1 = factory.create(inputA1); 103 | assert.equal(a1, expectedA1); 104 | }); 105 | 106 | it('#create: input as array', () => { 107 | // Create an array of objects of the same type. 108 | let inputA2 = { "_type": "TypeA", "value": 2}; 109 | let arrayA = factory.create([inputA1, inputA2]); 110 | assert.deepEqual(arrayA, [1, 2]); 111 | }); 112 | 113 | // Create an object that needs ObjectContext. 114 | // Create a simple context. 115 | var context: ObjectContext = { 116 | create: (input: any): any => { 117 | return factory.create(input); 118 | }, 119 | get: (name: string): NamedObject => { 120 | return null; 121 | }, 122 | forEach: (callback: (object: NamedObject) => void) => { 123 | // Do nothing. 124 | }, 125 | baseDir: __dirname 126 | } 127 | 128 | let inputB1 = {"_type": "TypeB", "value": inputA1}; 129 | it('#create: constructor needs a context object.', () => { 130 | assert.equal(factory.create(inputB1, context), expectedA1); 131 | }); 132 | 133 | it('#create: array input with different types.', () => { 134 | // Create an array of objects of different type. 135 | assert.throws(() => { 136 | factory.create([inputA1, inputB1], context); 137 | }, Error); 138 | }); 139 | 140 | it('#fromDefinition', () => { 141 | let defs: TypeDef[] = [{ 142 | typeName: "TypeA", 143 | moduleName: "./object-type-test", 144 | functionName: "createA" 145 | }]; 146 | factory = TypeRegistry.fromDefinition(defs, __dirname); 147 | assert(factory.supports('TypeA')); 148 | 149 | let inputA1 = { "_type": "TypeA", "value": 1}; 150 | assert.equal(factory.create(inputA1), 1); 151 | }); 152 | }); 153 | }); 154 | 155 | export function createA(input: {_type: "TypeA", value: number}) { 156 | return input.value; 157 | } -------------------------------------------------------------------------------- /test/test-app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-app", 3 | 4 | "description": "Test application", 5 | 6 | "allowPerRequestOverride": true, 7 | 8 | "defaultExecutionStack": [ 9 | "finalizeResponse", 10 | "executeEntryPoint" 11 | ], 12 | 13 | "objectTypes": [ 14 | "./object-types.json" 15 | ], 16 | "objectProviders": [ 17 | "./object-providers.json" 18 | ], 19 | "namedObjects": [ 20 | "./entrypoints.json", 21 | "./objects.json" 22 | ], 23 | "metrics": { 24 | "sectionName": "TestApp", 25 | "definition": [ 26 | "./metrics.json" 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /test/test-app/entrypoints.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "foo", 4 | "value": { 5 | "_type": "EntryPoint", 6 | "moduleName": "./test-app", 7 | "functionName": "entrypoints.foo" 8 | } 9 | }, 10 | { 11 | "name": "bar", 12 | "value": { 13 | "_type": "EntryPoint", 14 | "moduleName": "./test-app", 15 | "functionName": "entrypoints.bar", 16 | "executionStack": [ 17 | "logRequestResponse", 18 | "finalizeResponse", 19 | "executeEntryPoint" 20 | ] 21 | } 22 | }, 23 | { 24 | "name": "alwaysThrow", 25 | "value": { 26 | "_type": "EntryPoint", 27 | "moduleName": "./test-app", 28 | "functionName": "entrypoints.alwaysThrow" 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /test/test-app/level0.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "application": "testApp2", 3 | "overrideTypes": [ 4 | { 5 | "typeName": "TypeD", 6 | "description": "Override TypeD at level0", 7 | "moduleName": "./test-app", 8 | "functionName": "types.createD_l0" 9 | }, 10 | { 11 | "typeName": "TypeX", 12 | "description": "New TypeX at level0", 13 | "moduleName": "./test-app", 14 | "functionName": "types.createX_l0" 15 | } 16 | ], 17 | "overrideProviders": [ 18 | { 19 | "protocol": "ProtocolD", 20 | "description": "Override ProtocolD at level0", 21 | "moduleName": "./test-app", 22 | "functionName": "providers.provideD_l0" 23 | }, 24 | { 25 | "protocol": "ProtocolX", 26 | "description": "Override ProtocolD at level0", 27 | "moduleName": "./test-app", 28 | "functionName": "providers.provideX_l0" 29 | } 30 | ], 31 | "overrideObjects": [ 32 | { 33 | "name": "object6", 34 | "value": { 35 | "_type": "TypeC", 36 | "value": "abc" 37 | } 38 | }, 39 | { 40 | "name": "object7", 41 | "value": "ProtocolC:/abc" 42 | }, 43 | { 44 | "name": "object8", 45 | "value": { 46 | "_type": "TypeX", 47 | "value": "abc" 48 | } 49 | }, 50 | { 51 | "name": "object9", 52 | "value": "ProtocolX:/abc" 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /test/test-app/level1.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "./level0.template.json", 3 | "overrideTypes": [ 4 | { 5 | "typeName": "TypeB", 6 | "description": "Override TypeB at level1", 7 | "moduleName": "./test-app", 8 | "functionName": "types.createB_l1" 9 | }, 10 | { 11 | "typeName": "TypeX", 12 | "description": "New TypeX at level1", 13 | "moduleName": "./test-app", 14 | "functionName": "types.createX_l1" 15 | } 16 | ], 17 | "overrideProviders": [ 18 | { 19 | "protocol": "ProtocolB", 20 | "description": "Override ProtocolB at level1", 21 | "moduleName": "./test-app", 22 | "functionName": "providers.provideB_l1" 23 | }, 24 | { 25 | "protocol": "ProtocolX", 26 | "description": "Override ProtocolX at level1", 27 | "moduleName": "./test-app", 28 | "functionName": "providers.provideX_l1" 29 | } 30 | ], 31 | "overrideObjects": [ 32 | ] 33 | } -------------------------------------------------------------------------------- /test/test-app/metrics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "requestRateFoo", 4 | "type": "Rate", 5 | "displayName": "Request Rate Foo", 6 | "description": "Request rate of foo", 7 | "dimensionNames": [] 8 | } 9 | ] -------------------------------------------------------------------------------- /test/test-app/object-providers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "protocol": "ProtocolA", 4 | "description": "Protocol A", 5 | "moduleName": "./test-app", 6 | "functionName": "providers.provideA", 7 | "exampleUri": [ 8 | "ProtocolA://abcde" 9 | ] 10 | }, 11 | { 12 | "protocol": "ProtocolB", 13 | "description": "Protocol B", 14 | "moduleName": "./test-app", 15 | "functionName": "providers.provideB_app", 16 | "exampleUri": [ 17 | "ProtocolB://abcde" 18 | ] 19 | }, 20 | { 21 | "protocol": "ProtocolC", 22 | "description": "Protocol C", 23 | "moduleName": "./test-app", 24 | "functionName": "providers.provideC_app", 25 | "exampleUri": [ 26 | "ProtocolC://abcde" 27 | ] 28 | } 29 | ] -------------------------------------------------------------------------------- /test/test-app/object-types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "typeName": "TypeA", 4 | "description": "Test type A", 5 | "moduleName": "./test-app", 6 | "functionName": "types.createA", 7 | "exampleObjects": [ 8 | { 9 | "_type": "TypeA", 10 | "value": 1 11 | } 12 | ] 13 | }, 14 | { 15 | "typeName": "TypeB", 16 | "description": "Test type B", 17 | "moduleName": "./test-app", 18 | "functionName": "types.createB_app", 19 | "exampleObjects": [ 20 | { 21 | "_type": "TypeB", 22 | "value": "abc" 23 | } 24 | ] 25 | }, 26 | { 27 | "typeName": "TypeC", 28 | "description": "Test type C", 29 | "moduleName": "./test-app", 30 | "functionName": "types.createC_app", 31 | "exampleObjects": [ 32 | { 33 | "_type": "TypeC", 34 | "value": "abc" 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /test/test-app/objects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "object0", 4 | "value": { 5 | "_type": "TypeA", 6 | "value": "abc" 7 | } 8 | }, 9 | { 10 | "name": "object1", 11 | "value": "ProtocolA:/abc" 12 | }, 13 | { 14 | "name": "object2", 15 | "value": { 16 | "_type": "TypeB", 17 | "value": "abc" 18 | } 19 | }, 20 | { 21 | "name": "object3", 22 | "value": "ProtocolB:/abc" 23 | }, 24 | { 25 | "name": "object4b", 26 | "value": { 27 | "_type": "TypeC", 28 | "value": "abc" 29 | } 30 | }, 31 | { 32 | "name": "object5b", 33 | "value": "ProtocolC:/abc" 34 | } 35 | ] -------------------------------------------------------------------------------- /test/test-app/test-app.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import { RequestContext } from '../../lib/request-context'; 5 | import { Uri } from '../../lib/object-provider'; 6 | 7 | export namespace types { 8 | export function createA(input: {_type: "TypeA", value: string}): string { 9 | return "A:" + input.value; 10 | } 11 | 12 | export function createB_app(input: {_type: "TypeB", value: string}): string { 13 | return "B:app:" + input.value; 14 | } 15 | 16 | export function createB_l0(input: {_type: "TypeB", value: string}): string { 17 | return "B:l0:" + input.value; 18 | } 19 | 20 | export function createB_l1(input: {_type: "TypeC", value: string}): string { 21 | return "B:l1:" + input.value; 22 | } 23 | 24 | export function createB_request(input: {_type: "TypeC", value: string}): string { 25 | return "B:request:" + input.value; 26 | } 27 | 28 | export function createC_app(input: {_type: "TypeC", value: string}): string { 29 | return "C:app:" + input.value; 30 | } 31 | 32 | export function createC_l1(input: {_type: "TypeC", value: string}): string { 33 | return "C:l1:" + input.value; 34 | } 35 | 36 | export function createC_request(input: {_type: "TypeC", value: string}): string { 37 | return "C:request:" + input.value; 38 | } 39 | 40 | export function createD_l0(input: {_type: "TypeD", value: string}): string { 41 | return "D:l0:" + input.value; 42 | } 43 | 44 | export function createD_request(input: {_type: "TypeD", value: string}): string { 45 | return "D:request:" + input.value; 46 | } 47 | 48 | export function createE(input: {_type: "TypeE", value: string}): string { 49 | return "E:" + input.value; 50 | } 51 | 52 | export function createX_l0(input: {_type: "TypeE", value: string}): string { 53 | return "X:l0:" + input.value; 54 | } 55 | 56 | export function createX_l1(input: {_type: "TypeE", value: string}): string { 57 | return "X:l1:" + input.value; 58 | } 59 | } 60 | 61 | export namespace providers { 62 | export function provideA(uri: Uri): string { 63 | return "A:" + uri.path; 64 | } 65 | 66 | export function provideB_app(uri: Uri): string { 67 | return "B:app:" + uri.path; 68 | } 69 | 70 | export function provideB_l0(uri: Uri): string { 71 | return "B:l0:" + uri.path; 72 | } 73 | 74 | export function provideB_l1(uri: Uri): string { 75 | return "B:l1:" + uri.path; 76 | } 77 | 78 | export function provideB_request(uri: Uri): string { 79 | return "B:request:" + uri.path; 80 | } 81 | 82 | export function provideC_app(uri: Uri): string { 83 | return "C:app:" + uri.path; 84 | } 85 | 86 | export function provideC_l1(uri: Uri): string { 87 | return "C:l1:" + uri.path; 88 | } 89 | 90 | export function provideC_request(uri: Uri): string { 91 | return "C:request:" + uri.path; 92 | } 93 | 94 | export function provideD_l0(uri: Uri): string { 95 | return "D:l0:" + uri.path; 96 | } 97 | 98 | export function provideD_request(uri: Uri): string { 99 | return "D:request:" + uri.path; 100 | } 101 | 102 | export function provideE(uri: Uri): string { 103 | return "E:" + uri.path; 104 | } 105 | 106 | export function provideX_l0(uri: Uri): string { 107 | return "X:l0:" + uri.path; 108 | } 109 | 110 | export function provideX_l1(uri: Uri): string { 111 | return "X:l1:" + uri.path; 112 | } 113 | } 114 | 115 | export namespace entrypoints { 116 | export function foo(context: RequestContext, input: string): string { 117 | return input; 118 | } 119 | 120 | export function bar(context: RequestContext, input: string): Promise { 121 | return new Promise(resolve => { 122 | setTimeout(() => { 123 | resolve(input); 124 | }, 20); 125 | }); 126 | } 127 | 128 | export function alwaysThrow() { 129 | throw new Error("You hit an always-throw entrypoint."); 130 | } 131 | } -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "experimentalDecorators": true, 6 | "noImplicitAny": true, 7 | "declaration": false, 8 | "sourceMap": false, 9 | "lib": ["es2015"] 10 | } 11 | } -------------------------------------------------------------------------------- /test/utils-test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import * as utils from '../lib/utils'; 5 | import * as path from 'path'; 6 | import * as assert from 'assert'; 7 | 8 | describe('winery/utils', () => { 9 | let schema = new utils.JsonSchema( 10 | path.resolve(__dirname, 'config/utils-test.schema.json')); 11 | 12 | describe('JsonSchema', () => { 13 | it('#validate: valid input', () => { 14 | assert(schema.validate({ 15 | stringProp: "hello world", 16 | numberProp: 0, 17 | booleanProp: true, 18 | arrayProp: [1, 2], 19 | objectProp: { 20 | field1: 1, 21 | additionalField: "additional" 22 | } 23 | })); 24 | }); 25 | 26 | it('#validate: wrong type', () => { 27 | assert(!schema.validate({ 28 | stringProp: 1, 29 | numberProp: 0, 30 | booleanProp: true, 31 | arrayProp: [1, 2], 32 | objectProp: { 33 | field1: 1, 34 | additionalField: "additional" 35 | } 36 | })); 37 | }); 38 | 39 | it('#validate: missing required properties', () => { 40 | assert(!schema.validate({ 41 | numberProp: 0, 42 | booleanProp: true, 43 | arrayProp: [1, 2], 44 | objectProp: { 45 | field1: 1, 46 | } 47 | })); 48 | }); 49 | it('#validate: not-allowed additional properties', () => { 50 | assert(!schema.validate({ 51 | stringProp: "hello world", 52 | numberProp: 0, 53 | booleanProp: true, 54 | notAllowedExtra: "extra", 55 | arrayProp: [1, 2], 56 | objectProp: { 57 | field1: 1, 58 | additionalField: "additional" 59 | } 60 | })); 61 | }); 62 | }); 63 | 64 | describe('Json reading', () => { 65 | it('#parseJsonString: valid string, no schema check', () => { 66 | let value = utils.parseJsonString('{ "prop": 1 }'); 67 | assert.equal(value.prop, 1); 68 | }); 69 | 70 | it('#parseJsonString: valid string, comform with schema', () => { 71 | let jsonString = ` 72 | { 73 | "stringProp": "hi", 74 | "numberProp": 0, 75 | "booleanProp": true, 76 | "arrayProp": [ 77 | 1, 78 | 2 79 | ], 80 | "objectProp": { 81 | "field1": 1 82 | } 83 | } 84 | `; 85 | 86 | let value: any = undefined; 87 | assert.doesNotThrow(() => { 88 | value = utils.parseJsonString(jsonString, schema); 89 | }); 90 | assert.equal(value.stringProp, 'hi'); 91 | assert.equal(value.numberProp, 0); 92 | assert.equal(value.booleanProp, true); 93 | assert.deepEqual(value.arrayProp, [1, 2]); 94 | assert.deepEqual(value.objectProp, { 95 | field1: 1 96 | }); 97 | }); 98 | 99 | it('#parseJsonString: valid string, not comform with schema', () => { 100 | // Missing 'stringProp'. 101 | let jsonString = ` 102 | { 103 | "numberProp": 0, 104 | "booleanProp": true, 105 | "arrayProp": [ 106 | 1, 107 | 2 108 | ], 109 | "objectProp": { 110 | "field1": 1 111 | } 112 | } 113 | `; 114 | 115 | assert.throws(() => { 116 | utils.parseJsonString(jsonString, schema); 117 | }); 118 | }); 119 | 120 | it('#parseJsonString: allow comments', () => { 121 | let jsonString = ` 122 | { 123 | // This is a comment. 124 | "prop": 0 125 | } 126 | `; 127 | let value: any = undefined; 128 | //assert.doesNotThrow(() => { 129 | value = utils.parseJsonString(jsonString, null, true); 130 | //}); 131 | assert.equal(value.prop, 0); 132 | }); 133 | 134 | it('#parseJsonString: not allow comments', () => { 135 | let jsonString = ` 136 | { 137 | // This is a comment. 138 | "prop": 0 139 | } 140 | `; 141 | assert.throws(() => { 142 | utils.parseJsonString(jsonString, null, false); 143 | }); 144 | }); 145 | 146 | it('#parseJsonString: invalid JSON string', () => { 147 | let jsonString = ` 148 | { 149 | "prop": 0, 150 | } 151 | `; 152 | assert.throws(() => { 153 | utils.parseJsonString(jsonString); 154 | }); 155 | }); 156 | 157 | it('#parseJsonFile', () => { 158 | let value: any = undefined; 159 | 160 | assert.doesNotThrow(() => { 161 | value = utils.parseJsonFile( 162 | path.resolve(__dirname, "config/utils-test.json"), 163 | schema, 164 | true); 165 | }); 166 | assert.equal(value.stringProp, 'hi'); 167 | assert.equal(value.numberProp, 0); 168 | assert.equal(value.booleanProp, true); 169 | assert.deepEqual(value.arrayProp, [1, 2]); 170 | assert.deepEqual(value.objectProp, { 171 | field1: 1, 172 | additionalField: "additional" 173 | }); 174 | }); 175 | }); 176 | 177 | describe('Transform', () => { 178 | it('#RenameProperties', () => { 179 | let value: any = { 180 | strProp: "value", 181 | numProp: 1 182 | }; 183 | value = new utils.RenameProperties( { 184 | strProp: "stringProp", 185 | numProp: "numberProp" 186 | }).apply(value); 187 | 188 | assert.strictEqual(value.stringProp, "value"); 189 | assert.strictEqual(value.numberProp, 1); 190 | }); 191 | 192 | it('#SetDefaultValue', () => { 193 | let value: any = { 194 | stringProp: "value" 195 | }; 196 | value = new utils.SetDefaultValue( { 197 | optionalProp: true 198 | }).apply(value); 199 | 200 | assert.strictEqual(value.optionalProp, true); 201 | }); 202 | 203 | it('#TransformPropertyValues', () => { 204 | let value: any = { 205 | stringProp: "1" 206 | }; 207 | value = new utils.TransformPropertyValues( { 208 | stringProp: (text: string) => { return Number.parseInt(text); } 209 | }).apply(value); 210 | 211 | assert.strictEqual(value.stringProp, 1); 212 | }); 213 | 214 | it('#ChainableTransform', () => { 215 | let value: any = { 216 | strProp: "1" 217 | }; 218 | 219 | value = new utils.RenameProperties( { strProp: "stringProp"}).add( 220 | new utils.SetDefaultValue( {optionalProp: true})).add( 221 | new utils.TransformPropertyValues( { 222 | stringProp: (text: string) => { return Number.parseInt(text);} 223 | })).apply(value); 224 | 225 | assert.deepEqual(value, { 226 | stringProp: 1, 227 | optionalProp: true 228 | }); 229 | }) 230 | }); 231 | 232 | describe('Miscs', () => { 233 | it('#appendMessageOnException', () => { 234 | let extraMessage = "extra message."; 235 | try { 236 | utils.appendMessageOnException(extraMessage, () => { 237 | throw new Error("intentional error."); 238 | }) 239 | } 240 | catch (error) { 241 | console.log(error.message); 242 | assert(error.message.endsWith(extraMessage)); 243 | } 244 | }); 245 | 246 | it('#makePromiseIfNotAlready', () => { 247 | let funcReturnsPromise = (): Promise => { 248 | return Promise.resolve(1); 249 | }; 250 | 251 | let funcDoesnotReturnsPromise = (): number => { 252 | return 1; 253 | } 254 | 255 | let ret1 = utils.makePromiseIfNotAlready(funcReturnsPromise()); 256 | ret1.then((value: number) => { 257 | assert.equal(value, 1); 258 | }); 259 | 260 | let ret2 = utils.makePromiseIfNotAlready(funcDoesnotReturnsPromise()); 261 | ret2.then((value: number) => { 262 | assert.equal(value, 1); 263 | }) 264 | }); 265 | 266 | it('#loadFunction', () => { 267 | let moduleName = path.resolve(__dirname, 'utils-test'); 268 | let f1 = utils.loadFunction(moduleName, "func1"); 269 | assert.equal(func1, f1); 270 | 271 | let f2 = utils.loadFunction(moduleName, 'ns.func2'); 272 | assert.equal(ns.func2, f2); 273 | 274 | let f3 = utils.loadFunction(moduleName, 'ns.child.func3'); 275 | assert.equal(ns.child.func3, f3); 276 | 277 | let f4 = utils.loadFunction(moduleName, 'ns.A.method'); 278 | assert.equal(ns.A.method, f4); 279 | }); 280 | }); 281 | }); 282 | 283 | /// Functions for testing utils.loadFunction. 284 | export function func1(): number { 285 | return 0; 286 | } 287 | 288 | export namespace ns { 289 | export function func2(): void { 290 | } 291 | 292 | export namespace child { 293 | export function func3(): void { 294 | } 295 | } 296 | 297 | export class A { 298 | public static method(): void { 299 | } 300 | } 301 | } --------------------------------------------------------------------------------