Repository: serverless/serverless-azure-functions
Pull request #212 (2019-07-23 19:45:08)

feat: Path from x-azure-settings is included in function.json
In serverless.yml, you can now specify an API route within `x-azure-settings`, which will be added to the `function.json` file.

Resolves [AB#552]

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24751757/badge)](https://coveralls.io/builds/24751757)

Coverage increased (+0.2%) to 84.403% when pulling **29f4f0de1001a431575f74a987830ad17c15e4e0 on tabarlow/path-package** into **1c8e5bf6fad91b81e65038e69f33a4088fc3e643 on dev**.


Patch:
diff --git a/src/models/serverless.ts b/src/models/serverless.ts
index 727d8ff..c15adf1 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -58,6 +58,23 @@ export interface ServerlessAzureConfig {
   functions: any;
 }
 
+export interface ServerlessAzureFunctionConfig {
+  handler: string;
+  events: ServerlessAzureFunctionBindingConfig[];
+}
+
+export interface ServerlessAzureFunctionBindingConfig {
+  http?: boolean;
+  "x-azure-settings": ServerlessExtraAzureSettingsConfig;
+}
+
+export interface ServerlessExtraAzureSettingsConfig {
+  direction?: string;
+  route?: string;
+  name?: string;
+  authLevel?: string;
+}
+
 export interface ServerlessCommand {
   usage: string;
   lifecycleEvents: string[];
@@ -73,6 +90,7 @@ export interface ServerlessCommand {
 export interface ServerlessCommandMap {
   [command: string]: ServerlessCommand;
 }
+
 export interface ServerlessAzureOptions extends Serverless.Options {
   resourceGroup?: string;
 }
diff --git a/src/plugins/package/azurePackagePlugin.ts b/src/plugins/package/azurePackagePlugin.ts
index 9fe86c3..1d3ab57 100644
--- a/src/plugins/package/azurePackagePlugin.ts
+++ b/src/plugins/package/azurePackagePlugin.ts
@@ -18,7 +18,7 @@ export class AzurePackagePlugin extends AzureBasePlugin {
       "after:package:finalize": this.finalize.bind(this),
     };
 
-    this.packageService = new PackageService(this.serverless);
+    this.packageService = new PackageService(this.serverless, this.options);
   }
 
   private async setupProviderConfiguration(): Promise<void> {
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index 58d0d50..7b21371 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -2,7 +2,7 @@ import axios from "axios";
 import fs from "fs";
 import request from "request";
 import Serverless from "serverless";
-import { ServerlessAzureOptions } from "../models/serverless";
+import { ServerlessAzureOptions, ServerlessAzureFunctionConfig } from "../models/serverless";
 import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
 import { configConstants } from "../config";
 import { DeploymentConfig, ServerlessAzureConfig } from "../models/serverless";
@@ -181,7 +181,7 @@ export abstract class BaseService {
   /**
    * Get function objects
    */
-  protected slsFunctions() {
+  protected slsFunctions(): { [functionName: string]: ServerlessAzureFunctionConfig } {
     return this.serverless.service["functions"];
   }
 
diff --git a/src/services/offlineService.ts b/src/services/offlineService.ts
index 5661c83..35f62e3 100644
--- a/src/services/offlineService.ts
+++ b/src/services/offlineService.ts
@@ -16,10 +16,10 @@ export class OfflineService extends BaseService {
       }
     }),
   }
-  
+
   public constructor(serverless: Serverless, options: Serverless.Options) {
     super(serverless, options, false);
-    this.packageService = new PackageService(serverless);
+    this.packageService = new PackageService(serverless, options);
   }
 
   public async build() {
@@ -55,4 +55,4 @@ export class OfflineService extends BaseService {
     this.log("Make sure you have Azure Functions Core Tools installed");
     this.log("If not installed run 'npm i azure-functions-core-tools -g")
   }
-}
\ No newline at end of file
+}
diff --git a/src/services/packageService.ts b/src/services/packageService.ts
index 86a8d2d..e81ca47 100644
--- a/src/services/packageService.ts
+++ b/src/services/packageService.ts
@@ -2,12 +2,14 @@ import fs from "fs";
 import path from "path";
 import Serverless from "serverless";
 import { FunctionMetadata, Utils } from "../shared/utils";
+import { BaseService } from "./baseService";
 
 /**
  * Adds service packing support
  */
-export class PackageService {
-  public constructor(private serverless: Serverless) {
+export class PackageService extends BaseService {
+  public constructor(serverless: Serverless, options: Serverless.Options) {
+    super(serverless, options, false);
   }
 
   /**
@@ -82,13 +84,23 @@ export class PackageService {
     const functionJSON = functionMetadata.params.functionsJson;
     functionJSON.entryPoint = functionMetadata.entryPoint;
     functionJSON.scriptFile = functionMetadata.handlerPath;
+    const functionObject = this.slsFunctions()[functionName];
+    const bindingAzureSettings = Utils.getIncomingBindingConfig(functionObject)["x-azure-settings"];
+    if (bindingAzureSettings.route) {
+      // Find incoming binding within functionJSON and set the route
+      const index = (functionJSON.bindings as any[])
+        .findIndex((binding) => (!binding.direction || binding.direction === "in"));
+      functionJSON.bindings[index].route = bindingAzureSettings.route;
+    }
 
     const functionDirPath = path.join(this.serverless.config.servicePath, functionName);
     if (!fs.existsSync(functionDirPath)) {
       fs.mkdirSync(functionDirPath);
     }
 
-    fs.writeFileSync(path.join(functionDirPath, "function.json"), JSON.stringify(functionJSON, null, 2));
+    const functionJsonString = JSON.stringify(functionJSON, null, 2);
+
+    fs.writeFileSync(path.join(functionDirPath, "function.json"), functionJsonString);
 
     return Promise.resolve();
   }
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 0fa71d8..7490d1c 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -3,6 +3,7 @@ import Serverless from "serverless";
 import { BindingUtils } from "./bindings";
 import { constants } from "./constants";
 import { Guard } from "./guard";
+import { ServerlessAzureFunctionConfig } from "../models/serverless";
 
 export interface FunctionMetadata {
   entryPoint: any;
@@ -212,4 +213,18 @@ export class Utils {
     }
     return match[1];
   }
+
+  public static getIncomingBindingConfig(functionConfig: ServerlessAzureFunctionConfig) {
+    return functionConfig.events.find((event) => {
+      const settings = event["x-azure-settings"]
+      return settings && (!settings.direction || settings.direction === "in");
+    });
+  }
+
+  public static getOutgoingBinding(functionConfig: ServerlessAzureFunctionConfig) {
+    return functionConfig.events.find((event) => {
+      const settings = event["x-azure-settings"]
+      return settings && settings.direction === "out";
+    });
+  }
 }


Test patch:
diff --git a/src/services/packageService.test.ts b/src/services/packageService.test.ts
index 4e06755..be57599 100644
--- a/src/services/packageService.test.ts
+++ b/src/services/packageService.test.ts
@@ -9,12 +9,15 @@ import { FunctionMetadata } from "../shared/utils";
 describe("Package Service", () => {
   let sls: Serverless;
   let packageService: PackageService;
+  const functionRoute = "myRoute";
 
   beforeEach(() => {
     sls = MockFactory.createTestServerless();
     sls.config.servicePath = process.cwd();
-
-    packageService = new PackageService(sls);
+    sls.service["functions"] = {
+      hello: MockFactory.createTestAzureFunctionConfig(functionRoute),
+    }
+    packageService = new PackageService(sls, MockFactory.createTestServerlessOptions());
   });
 
   afterEach(() => {
@@ -71,17 +74,34 @@ describe("Package Service", () => {
   });
 
   it("createBinding writes function.json files into function folder", async () => {
-    const functionName = "helloWorld";
+    const functionName = "hello";
     const functionMetadata: FunctionMetadata = {
       entryPoint: "handler",
-      handlerPath: "src/handlers/hello.js",
+      handlerPath: "src/handlers/hello",
       params: {
-        functionsJson: {},
+        functionsJson: {
+          bindings: [
+            MockFactory.createTestHttpBinding("out"),
+            MockFactory.createTestHttpBinding("in"),
+          ]
+        }
       },
     };
 
     const expectedFolderPath = path.join(sls.config.servicePath, functionName);
     const expectedFilePath = path.join(expectedFolderPath, "function.json");
+    const expectedFunctionJson = {
+      entryPoint: "handler",
+      scriptFile: "src/handlers/hello",
+      bindings: [
+        MockFactory.createTestHttpBinding("out"),
+        {
+          ...MockFactory.createTestHttpBinding("in"),
+          route: functionRoute
+        }
+      ]
+
+    }
 
     mockFs({});
 
@@ -91,19 +111,26 @@ describe("Package Service", () => {
     await packageService.createBinding(functionName, functionMetadata);
 
     expect(mkdirSpy).toBeCalledWith(expectedFolderPath);
-    expect(writeFileSpy).toBeCalledWith(expectedFilePath, expect.any(String));
+    const call = writeFileSpy.mock.calls[0];
+    expect(call[0]).toEqual(expectedFilePath);
+    expect(JSON.parse(call[1])).toEqual(expectedFunctionJson);
 
     mkdirSpy.mockRestore();
     writeFileSpy.mockRestore();
   });
 
   it("createBinding does not need to create directory if function folder already exists", async () => {
-    const functionName = "helloWorld";
+    const functionName = "hello";
     const functionMetadata: FunctionMetadata = {
       entryPoint: "handler",
-      handlerPath: "src/handlers/hello.js",
+      handlerPath: "src/handlers/hello",
       params: {
-        functionsJson: {},
+        functionsJson: {
+          bindings: [
+            MockFactory.createTestHttpBinding("out"),
+            MockFactory.createTestHttpBinding("in"),
+          ]
+        },
       },
     };
 
@@ -111,7 +138,7 @@ describe("Package Service", () => {
     const expectedFilePath = path.join(expectedFolderPath, "function.json");
 
     mockFs({
-      "helloWorld": {
+      "hello": {
         "index.js": "contents",
       },
     });
@@ -162,4 +189,4 @@ describe("Package Service", () => {
     mkDirSpy.mockRestore();
     copyFileSpy.mockRestore();
   });
-});
\ No newline at end of file
+});
diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts
index 3e6feda..4a121df 100644
--- a/src/shared/utils.test.ts
+++ b/src/shared/utils.test.ts
@@ -162,6 +162,7 @@ describe("utils", () => {
     const actual = Utils.getNormalizedRegionName(expected);
     expect(actual).toEqual(expected);
   });
+
   it("should get a timestamp from a name", () => {
     expect(Utils.getTimestampFromName("myDeployment-t12345")).toEqual("12345");
     expect(Utils.getTimestampFromName("myDeployment-t678987645")).toEqual("678987645");
@@ -169,5 +170,23 @@ describe("utils", () => {
 
     expect(Utils.getTimestampFromName("myDeployment-t")).toEqual(null);
     expect(Utils.getTimestampFromName("")).toEqual(null);
-  })
+  });
+
+  it("should get incoming binding", () => {
+    expect(Utils.getIncomingBindingConfig(MockFactory.createTestAzureFunctionConfig())).toEqual(
+      {
+        http: true,
+        "x-azure-settings": MockFactory.createTestHttpBinding("in"),
+      }
+    );
+  });
+
+  it("should get outgoing binding", () => {
+    expect(Utils.getOutgoingBinding(MockFactory.createTestAzureFunctionConfig())).toEqual(
+      {
+        http: true,
+        "x-azure-settings": MockFactory.createTestHttpBinding("out"),
+      }
+    );
+  });
 });
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index 2d48e2f..ed5f074 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -15,7 +15,7 @@ import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement";
 import { ArmDeployment, ArmResourceTemplate } from "../models/armTemplates";
 import { ServicePrincipalEnvVariables } from "../models/azureProvider";
 import { Logger } from "../models/generic";
-import { ServerlessAzureConfig, ServerlessAzureProvider } from "../models/serverless";
+import { ServerlessAzureConfig, ServerlessAzureProvider, ServerlessAzureFunctionConfig } from "../models/serverless";
 
 function getAttribute(object: any, prop: string, defaultValue: any): any {
   if (object && object[prop]) {
@@ -394,18 +394,35 @@ export class MockFactory {
     return bindings;
   }
 
+  public static createTestAzureFunctionConfig(route?: string): ServerlessAzureFunctionConfig {
+    return {
+      events: [
+        {
+          http: true,
+          "x-azure-settings": MockFactory.createTestHttpBinding("in", route),
+        },
+        {
+          http: true,
+          "x-azure-settings": MockFactory.createTestHttpBinding("out"),
+        }
+      ],
+      handler: "handler.js",
+    }
+  }
+
   public static createTestBinding() {
     // Only supporting HTTP for now, could support others
     return MockFactory.createTestHttpBinding();
   }
 
-  public static createTestHttpBinding(direction: string = "in") {
+  public static createTestHttpBinding(direction: string = "in", route?: string) {
     if (direction === "in") {
       return {
         authLevel: "anonymous",
         type: "httpTrigger",
         direction,
         name: "req",
+        route,
       }
     } else {
       return {