VoxaAI/voxa-cli

View on GitHub
src/Schema.ts

Summary

Maintainability
C
1 day
Test Coverage
/*
 * Copyright (c) 2018 Rain Agency <contact@rain.agency>
 * Author: Rain Agency <contact@rain.agency>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
/* tslint:disable:forin */
import _ from "lodash";
import path from "path";
import {
  downloadProcessor,
  intentUtterProcessor,
  invocationProcessor,
  publishingProcessor,
  slotProcessor,
  viewsProcessor
} from "./Processor";
import { IVoxaSheet } from "./VoxaSheet";

export abstract class Schema {
  public intents: IIntent[] = [];
  public slots: ISlot[] = [];
  public downloads: IDownload[] = [];
  public AVAILABLE_LOCALES: string[];
  public fileContent: IFileContent[] = [];
  public views: IView[] = [];
  public invocations: IInvocation[] = [];
  public publishing: IPublishingInformation[] = [];

  public NAMESPACE: string;
  public interactionOptions = {} as any;

  public constructor(
    namespace: string = "alexa",
    availableLocales: string[] = ["en-US"],
    voxaSheets: IVoxaSheet[],
    interactionOption: any
  ) {
    this.AVAILABLE_LOCALES = availableLocales;
    this.NAMESPACE = namespace;
    this.interactionOptions = interactionOption;
    this.publishing = publishingProcessor(voxaSheets, this.AVAILABLE_LOCALES);
    this.intents = intentUtterProcessor(voxaSheets, this.AVAILABLE_LOCALES);
    this.downloads = downloadProcessor(voxaSheets, this.AVAILABLE_LOCALES);
    this.views = viewsProcessor(voxaSheets, this.AVAILABLE_LOCALES);
    this.slots = slotProcessor(voxaSheets, this.AVAILABLE_LOCALES);
    this.invocations = invocationProcessor(voxaSheets, this.AVAILABLE_LOCALES);
  }

  public abstract validate(locale: string, environment: string): void;
  public abstract build(locale: string, environment: string): void; // must be implemented in derived classes

  public intentsByPlatformAndEnvironments(locale: string, environment: string): IIntent[] {
    return this.intents.reduce((acc: any, intent: any) => {
      const diffLocale = locale !== intent.locale;

      if (diffLocale) {
        return acc;
      }

      if (!_.isEmpty(intent.platforms) && !intent.platforms.includes(this.NAMESPACE)) {
        return acc;
      }

      if (!_.isEmpty(intent.environments) && !intent.environments.includes(environment)) {
        return acc;
      }
      acc.push(intent);

      return acc;
    }, []);
  }

  public buildDownloads(): void {
    this.downloads.forEach(download => {
      const file: IFileContent = {
        path: this.buildFilePath(
          this.interactionOptions.viewsPath,
          `${download.locale}/${_.kebabCase(download.name)}.json`
        ),

        content: download.data
      };
      this.fileContent.push(file);
    });
  }

  public buildViewsMapping(): void {
    function pathsFinder(object: any, prefixes: string[] = []) {
      let paths: string[] = [];
      let value: any;
      if (typeof object === "object") {
        for (const key in object) {
          value = object[key];
          if (typeof value === "object" && !Array.isArray(value)) {
            paths = paths.concat(pathsFinder(value, prefixes.concat([key])));
          } else {
            paths.push(prefixes.concat(key).join("."));
          }
        }
      }
      return paths;
    }

    function variablesFinder(data: any, viewPath: string) {
      let variables;
      let value = _.get(data, viewPath);

      if (_.isString(value)) {
        value = [value];
      }

      if (_.isArray(value)) {
        variables = _.chain(value)
          .filter(v => _.isString(v))
          .map(v => v.match(/{([\s\S]+?)}/g))
          .flatten()
          .compact()
          .map(v => v.replace("}", "").replace("{", ""))
          .value();
      }

      return variables;
    }

    const viewsContent = _.chain(this.views)
      .map(view => pathsFinder(view.data).map(viewPath => ({ locale: view.locale, viewPath })))
      .flatten()
      .reduce(
        (acc, next) => {
          const locales = _.get(acc, next.viewPath, []);
          locales.push(next.locale);
          acc[next.viewPath] = _.uniq(locales);
          return acc;
        },
        {} as any
      )
      .value();

    const variablesContent = _.chain(this.views)
      .map(view =>
        pathsFinder(view.data).map(viewPath => ({
          locale: view.locale,
          variables: variablesFinder(view.data, viewPath)
        }))
      )
      .flatten()
      .reduce(
        (acc, next) => {
          if (_.isArray(next.variables)) {
            next.variables.map(variable => {
              const locales = _.get(acc, variable, []);
              locales.push(next.locale);
              acc[variable] = _.uniq(locales);
            });
          }

          return acc;
        },
        {} as any
      )
      .value();

    const fileViewMap: IFileContent = {
      path: this.buildFilePath(this.interactionOptions.viewsPath, "views.map.json"),
      content: viewsContent
    };

    const fileVariablesMap: IFileContent = {
      path: this.buildFilePath(this.interactionOptions.viewsPath, "variables.map.json"),
      content: variablesContent
    };

    this.fileContent.push(fileViewMap, fileVariablesMap);
  }

  public buildViews(): void {
    const viewsContent = _.chain(this.views)
      .map(view => [view.locale, { translation: view.data }])
      .fromPairs()
      .value();

    const file: IFileContent = {
      path: this.buildFilePath(this.interactionOptions.viewsPath, "views.json"),
      content: viewsContent
    };
    this.fileContent.push(file);
  }

  public buildSynonyms(): void {
    this.slots.forEach(slot => {
      const file: IFileContent = {
        path: this.buildFilePath(
          this.interactionOptions.viewsPath,
          "synonyms",
          `${slot.locale}/${_.kebabCase(slot.name)}.json`
        ),
        content: slot.values.reduce(
          (acc: any, next: any) => {
            if (_.isEmpty(next.synonyms)) {
              acc[next.value] = next.value;
            } else {
              next.synonyms.map((syn: any) => {
                acc[syn] = next.value;
              });
            }
            return acc;
          },
          {} as any
        )
      };
      this.fileContent.push(file);
    });
  }

  public getIntentsDefinition(locale: string, environment: string) {
    const intentsByPlatformAndEnvironments = this.intentsByPlatformAndEnvironments(
      locale,
      environment
    );

    const intents = intentsByPlatformAndEnvironments.map((rawIntent: IIntent) => {
      const { name, samples, slotsDefinition } = rawIntent;
      const slots = _(slotsDefinition)
        .filter(slot => this.filterByPlatform(slot))
        .map((slot: any) => {
          return {
            type: slot.type,
            name: slot.name.replace("{", "").replace("}", ""),
            samples: slot.samples.length > 0 ? slot.samples : undefined
          };
        })
        .value();

      return { name, samples, slots };
    });

    return intents;
  }

  public getSlotsByIntentsDefinition(locale: string, environment: string): ISlot[] {
    const intents = this.getIntentsDefinition(locale, environment);
    const slotsDefinitionNames = _.chain(intents)
      .map("slots")
      .flatten()
      .map("type")
      .uniq()
      .value();

    return this.slots.filter(
      slot => slot.locale === locale && _.includes(slotsDefinitionNames, slot.name)
    );
  }
  public mergeManifest(environment: string): any {
    const manifest = {};
    const NAMESPACE = this.NAMESPACE;
    this.publishing
      .filter(item => _.isEmpty(item.environments) && item.key.includes(this.NAMESPACE))
      .forEach(assignPublishingKeys);

    this.publishing
      .filter(
        item => _.includes(item.environments, environment) && item.key.includes(this.NAMESPACE)
      )
      .forEach(assignPublishingKeys);

    return manifest;

    function assignPublishingKeys(item: IPublishingInformation) {
      let { key, value } = item;
      key = key.replace(`${NAMESPACE}.`, "");

      if (key.includes("[]")) {
        const keySplitByArray = key.split("[]");
        key = keySplitByArray[0];
        const subKey = keySplitByArray[1];

        const arrayOnPublishingInformation = _.get(manifest, key, []);

        if (!_.isEmpty(subKey)) {
          const subObject = {};
          _.set(subObject, subKey, value);
          arrayOnPublishingInformation.push(subObject);
        } else {
          arrayOnPublishingInformation.push(value);
        }
        value = arrayOnPublishingInformation;
      }
      if (key.includes("keywords") && _.isString(value)) {
        value = value.split(",").map(_.trim);
      }
      _.set(manifest, key, value);
    }
  }

  protected filterByPlatform(slot: ISlotDefinition) {
    return slot.platform === this.NAMESPACE || slot.platform === undefined;
  }

  protected buildFilePath(...names: string[]): string {
    return path.join(this.interactionOptions.rootPath, ...names);
  }
}

export interface IIntent {
  name: string;
  samples: string[];
  responses: string[];
  confirmations: string[];
  slotsDefinition: ISlotDefinition[];
  canFulfillIntent: boolean;
  webhookUsed: boolean;
  webhookForSlotFilling: boolean;
  startIntent: boolean;
  signInRequired: boolean;
  endIntent: boolean;
  events: string[] | IEvent[];
  environments: string[];
  platforms: string[];
  locale: string;
  delegationStrategy?: "ALWAYS" | "SKILL_RESPONSE";
  confirmationRequired: boolean;
  parameterName?: string;
  parameterValue?: string;
}

export interface IEvent {
  name: string;
}

export interface IFileContent {
  path: string;
  content: any;
  // promise?: Promise<void>;
}

export interface IInvocation {
  locale: string;
  name: string;
  environment: string;
}

export interface ISlotDefinition {
  name: string;
  type: string;
  platform?: string;
  required: boolean;
  requiresConfirmation: boolean;
  requiresElicitation: boolean;
  samples: string[];
  prompts: {
    confirmation: string[];
    elicitation: string[];
  };
}

export interface ISlot {
  locale: string;
  name: string; // LIST_OF_COUNTRIES
  values: ISlotSynonymns[];
}
export interface ISlotSynonymns {
  value: string; // united-states
  synonyms: string[]; // USA, united states
}

export interface IView {
  locale: string;
  data: {};
}

export interface IDownload {
  name: string;
  data: {}[];
  locale: string;
}

export interface IPublishingInformation {
  key: string;
  value: string | object;
  environments: string[];
}