From caf260c452a1b254103bc7b5c8a89690b3b60703 Mon Sep 17 00:00:00 2001 From: Dakshraj Sharma Date: Thu, 27 May 2021 01:18:23 +0530 Subject: [PATCH 1/2] adds support for glob-style input spec paths and directory output paths --- bin/cli.js | 101 +- package-lock.json | 41 +- package.json | 3 +- tests/bin/cli.test.ts | 10 + tests/bin/expected/manifold.ts | 1010 +++++++++++++++++++ tests/bin/expected/petstore.ts | 465 +++++++++ tests/bin/specs/manifold.yaml | 1674 ++++++++++++++++++++++++++++++++ 7 files changed, 3273 insertions(+), 31 deletions(-) create mode 100644 tests/bin/expected/manifold.ts create mode 100644 tests/bin/expected/petstore.ts create mode 100644 tests/bin/specs/manifold.yaml diff --git a/bin/cli.js b/bin/cli.js index d9c0f48f9..02aeaff55 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -4,6 +4,7 @@ const fs = require("fs"); const { bold, green, red } = require("kleur"); const path = require("path"); const meow = require("meow"); +const glob = require("tiny-glob"); const { default: openapiTS } = require("../dist/cjs/index.js"); const { loadSpec } = require("./loaders"); @@ -52,36 +53,31 @@ Options } ); +const OUTPUT_FILE = "FILE"; +const OUTPUT_STDOUT = "STDOUT"; + const timeStart = process.hrtime(); -async function main() { - let output = "FILE"; // FILE or STDOUT - const pathToSpec = cli.input[0]; +function errorAndExit(errorMessage) { + process.exitCode = 1; // needed for async functions + throw new Error(red(errorMessage)); +} - // 0. setup - if (!cli.flags.output) { - output = "STDOUT"; // if --output not specified, fall back to stdout - } - if (output === "FILE") { - console.info(bold(`✨ openapi-typescript ${require("../package.json").version}`)); // only log if we’re NOT writing to stdout - } - if (cli.flags.rawSchema && !cli.flags.version) { - throw new Error(`--raw-schema requires --version flag`); - } +async function generateSchema(pathToSpec) { + const output = cli.flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT - // 1. input + // load spec let spec = undefined; try { spec = await loadSpec(pathToSpec, { auth: cli.flags.auth, - log: output !== "STDOUT", + log: output !== OUTPUT_STDOUT, }); } catch (err) { - process.exitCode = 1; // needed for async functions - throw new Error(red(`❌ ${err}`)); + errorAndExit(`❌ ${err}`); } - // 2. generate schema (the main part!) + // generate schema const result = openapiTS(spec, { auth: cli.flags.auth, additionalProperties: cli.flags.additionalProperties, @@ -91,11 +87,54 @@ async function main() { version: cli.flags.version, }); - // 3. output - if (output === "FILE") { - // output option 1: file - const outputFile = path.resolve(process.cwd(), cli.flags.output); + // output + if (output === OUTPUT_FILE) { + let outputFile = path.resolve(process.cwd(), cli.flags.output); + + // decide filename if outputFile is a directory + if (fs.existsSync(outputFile) && fs.lstatSync(outputFile).isDirectory()) { + const basename = path.basename(pathToSpec).split(".").slice(0, -1).join(".") + ".ts"; + outputFile = path.resolve(outputFile, basename); + } + + fs.writeFileSync(outputFile, result, "utf8"); + + const timeEnd = process.hrtime(timeStart); + const time = timeEnd[0] + Math.round(timeEnd[1] / 1e6); + console.log(green(`🚀 ${pathToSpec} -> ${bold(outputFile)} [${time}ms]`)); + } else { + process.stdout.write(result); + } + + return result; +} + +async function main() { + const output = cli.flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT + const pathToSpec = cli.input[0]; + const inputSpecPaths = await glob(pathToSpec, { filesOnly: true }); + + if (output === OUTPUT_FILE) { + console.info(bold(`✨ openapi-typescript ${require("../package.json").version}`)); // only log if we’re NOT writing to stdout + } + + if (cli.flags.rawSchema && !cli.flags.version) { + throw new Error(`--raw-schema requires --version flag`); + } + + if (/^https?:\/\//.test(pathToSpec)) { + // handle remote resource input and exit + return await generateSchema(pathToSpec); + } + + // no matches for glob + if (inputSpecPaths.length === 0) { + errorAndExit( + `❌ Could not find any spec files matching the provided input path glob. Please check that the path is correct.` + ); + } + if (output === OUTPUT_FILE) { // recursively create parent directories if they don’t exist const parentDirs = cli.flags.output.split(path.sep); for (var i = 1; i < parentDirs.length; i++) { @@ -104,15 +143,19 @@ async function main() { fs.mkdirSync(dir); } } + } - fs.writeFileSync(outputFile, result, "utf8"); + // if there are multiple specs, ensure that output is a directory + if (inputSpecPaths.length > 1 && output === OUTPUT_FILE && !fs.lstatSync(cli.flags.output).isDirectory()) { + errorAndExit( + `❌ When specifying a glob matching multiple input specs, you must specify a directory for generated type definitions.` + ); + } - const timeEnd = process.hrtime(timeStart); - const time = timeEnd[0] + Math.round(timeEnd[1] / 1e6); - console.log(green(`🚀 ${pathToSpec} -> ${bold(cli.flags.output)} [${time}ms]`)); - } else { - // output option 2: stdout - process.stdout.write(result); + let result = ""; + for (const specPath of inputSpecPaths) { + // append result returned for each spec + result += await generateSchema(specPath); } return result; diff --git a/package-lock.json b/package-lock.json index 4b20f60d8..8f411da4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "kleur": "^4.1.4", "meow": "^9.0.0", "mime": "^2.5.2", - "prettier": "^2.3.0" + "prettier": "^2.3.0", + "tiny-glob": "^0.2.9" }, "bin": { "openapi-typescript": "bin/cli.js" @@ -3236,6 +3237,11 @@ "node": ">=4" } }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, "node_modules/globby": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", @@ -3265,6 +3271,11 @@ "node": ">= 4" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, "node_modules/graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -6933,6 +6944,15 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "dev": true }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, "node_modules/tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -10128,6 +10148,11 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, "globby": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", @@ -10150,6 +10175,11 @@ } } }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -13068,6 +13098,15 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "dev": true }, + "tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "requires": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", diff --git a/package.json b/package.json index cc74b9672..bff03b052 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "kleur": "^4.1.4", "meow": "^9.0.0", "mime": "^2.5.2", - "prettier": "^2.3.0" + "prettier": "^2.3.0", + "tiny-glob": "^0.2.9" }, "devDependencies": { "@types/jest": "^26.0.14", diff --git a/tests/bin/cli.test.ts b/tests/bin/cli.test.ts index c4e7a383d..18d3cfe6d 100644 --- a/tests/bin/cli.test.ts +++ b/tests/bin/cli.test.ts @@ -31,4 +31,14 @@ describe("cli", () => { const result = execSync(`../../bin/cli.js specs/petstore.yaml`, { cwd: __dirname }); expect(result.toString("utf8")).toBe(expected); }); + + it("supports glob paths", () => { + const expectedPetstore = fs.readFileSync(path.join(__dirname, "expected", "petstore.ts"), "utf8"); + const expectedManifold = fs.readFileSync(path.join(__dirname, "expected", "manifold.ts"), "utf8"); + execSync(`../../bin/cli.js \"specs/*.yaml\" -o generated/`, { cwd: __dirname }); // Quotes are necessary because shells like zsh treats glob weirdly + const outputPetstore = fs.readFileSync(path.join(__dirname, "generated", "petstore.ts"), "utf8"); + const outputManifold = fs.readFileSync(path.join(__dirname, "generated", "manifold.ts"), "utf8"); + expect(outputPetstore).toBe(expectedPetstore); + expect(outputManifold).toBe(expectedManifold); + }); }); diff --git a/tests/bin/expected/manifold.ts b/tests/bin/expected/manifold.ts new file mode 100644 index 000000000..821bda220 --- /dev/null +++ b/tests/bin/expected/manifold.ts @@ -0,0 +1,1010 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/regions/": { + get: { + parameters: { + query: { + /** Filter results to only include the regions that have this location. */ + location?: string; + /** + * Filter results to only include the regions that are on this + * platform. + */ + platform?: string; + }; + }; + responses: { + /** A list of regions. */ + 200: { + schema: definitions["Region"][]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + post: { + parameters: { + body: { + /** Region create request */ + body: definitions["CreateRegion"]; + }; + }; + responses: { + /** Complete region object */ + 201: { + schema: definitions["Region"]; + }; + /** Invalid request provided */ + 400: { + schema: definitions["Error"]; + }; + /** Region already exists for that platform and location */ + 409: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/regions/{id}": { + get: { + parameters: { + path: { + /** ID of the region to lookup, stored as a base32 encoded 18 byte identifier. */ + id: string; + }; + }; + responses: { + /** A region. */ + 200: { + schema: definitions["Region"]; + }; + /** Provided Region ID is Invalid */ + 400: { + schema: definitions["Error"]; + }; + /** Region could not be found */ + 404: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + patch: { + parameters: { + path: { + /** ID of the region to lookup, stored as a base32 encoded 18 byte identifier. */ + id: string; + }; + body: { + /** Region update request */ + body: definitions["UpdateRegion"]; + }; + }; + responses: { + /** Complete region object */ + 200: { + schema: definitions["Region"]; + }; + /** Invalid request provided */ + 400: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/providers/": { + get: { + parameters: { + query: { + /** Filter results to only include those that have this label. */ + label?: parameters["LabelFilter"]; + }; + }; + responses: { + /** A list of providers. */ + 200: { + schema: definitions["Provider"][]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + post: { + parameters: { + body: { + /** Provider create request */ + body: definitions["CreateProvider"]; + }; + }; + responses: { + /** Complete provider object */ + 201: { + schema: definitions["Provider"]; + }; + /** Invalid request provided */ + 400: { + schema: definitions["Error"]; + }; + /** Forbidden */ + 403: { + schema: definitions["Error"]; + }; + /** Provider already exists with that label */ + 409: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/providers/{id}": { + get: { + parameters: { + path: { + /** ID of the provider to lookup, stored as a base32 encoded 18 byte identifier. */ + id: string; + }; + }; + responses: { + /** A provider. */ + 200: { + schema: definitions["Provider"]; + }; + /** Unknown provider error */ + 404: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + patch: { + parameters: { + path: { + /** ID of the provider to update, stored as a base32 encoded 18 byte identifier. */ + id: string; + }; + body: { + /** Provider update request */ + body: definitions["UpdateProvider"]; + }; + }; + responses: { + /** Complete provider object */ + 200: { + schema: definitions["Provider"]; + }; + /** Invalid request provided */ + 400: { + schema: definitions["Error"]; + }; + /** Forbidden */ + 403: { + schema: definitions["Error"]; + }; + /** Provider not found */ + 404: { + schema: definitions["Error"]; + }; + /** Provider already exists with that label */ + 409: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/products/": { + get: { + parameters: { + query: { + /** + * Base32 encoded 18 byte identifier of the provider that these + * products must belong to. + */ + provider_id?: string; + /** Filter results to only include those that have this label. */ + label?: parameters["LabelFilter"]; + /** Return only products matching at least one of the tags. */ + tags?: string[]; + }; + }; + responses: { + /** A product. */ + 200: { + schema: definitions["Product"][]; + }; + /** Invalid provider_id supplied */ + 400: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + post: { + parameters: { + body: { + /** Product create request */ + body: definitions["CreateProduct"]; + }; + }; + responses: { + /** Complete product object */ + 201: { + schema: definitions["Product"]; + }; + /** Invalid request provided */ + 400: { + schema: definitions["Error"]; + }; + /** Forbidden */ + 403: { + schema: definitions["Error"]; + }; + /** Product already exists with that label */ + 409: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/internal/products": { + get: { + parameters: { + query: { + /** + * Base32 encoded 18 byte identifier of the provider that these + * products must belong to. + */ + provider_id?: string; + /** Filter results to only include those that have this label. */ + label?: parameters["LabelFilter"]; + /** Return only products matching at least one of the tags. */ + tags?: string[]; + /** Return product listings without plan information */ + include_plans?: boolean; + }; + }; + responses: { + /** A product. */ + 200: { + schema: definitions["ExpandedProduct"][]; + }; + /** Invalid provider_id supplied */ + 400: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/products/{id}": { + get: { + parameters: { + path: { + /** + * ID of the product to lookup, stored as a base32 encoded 18 byte + * identifier. + */ + id: string; + }; + }; + responses: { + /** A product. */ + 200: { + schema: definitions["Product"]; + }; + /** Invalid Product ID */ + 400: { + schema: definitions["Error"]; + }; + /** Product not found error */ + 404: { + schema: definitions["Error"]; + }; + /** Unexpected error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + patch: { + parameters: { + path: { + /** + * ID of the product to lookup, stored as a base32 encoded 18 byte + * identifier. + */ + id: string; + }; + body: { + /** Product update request */ + body: definitions["UpdateProduct"]; + }; + }; + responses: { + /** Complete product object */ + 200: { + schema: definitions["Product"]; + }; + /** Invalid Product ID */ + 400: { + schema: definitions["Error"]; + }; + /** Product not found error */ + 404: { + schema: definitions["Error"]; + }; + /** Unexpected error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/plans/{id}": { + get: { + parameters: { + path: { + /** + * ID of the plan to lookup, stored as a base32 encoded 18 byte + * identifier. + */ + id: string; + }; + }; + responses: { + /** A plan. */ + 200: { + schema: definitions["ExpandedPlan"]; + }; + /** Invalid Plan ID Provided */ + 400: { + schema: definitions["Error"]; + }; + /** Unknown plan error */ + 404: { + schema: definitions["Error"]; + }; + /** Unexpected error */ + default: { + schema: definitions["Error"]; + }; + }; + }; + patch: { + parameters: { + path: { + /** + * ID of the plan to lookup, stored as a base32 encoded 18 byte + * identifier. + */ + id: string; + }; + body: { + /** Plan update request */ + body: definitions["UpdatePlan"]; + }; + }; + responses: { + /** Complete product plan */ + 200: { + schema: definitions["Plan"]; + }; + /** Invalid Plan ID */ + 400: { + schema: definitions["Error"]; + }; + /** Plan not found error */ + 404: { + schema: definitions["Error"]; + }; + /** Unexpected error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; + "/plans/": { + get: { + parameters: { + query: { + /** Return the plans that are associated with this product. */ + product_id: string[]; + /** Filter results to only include those that have this label. */ + label?: parameters["LabelFilter"]; + }; + }; + responses: { + /** A list of plans for the given product. */ + 200: { + schema: definitions["ExpandedPlan"][]; + }; + /** Invalid Parameters Provided */ + 400: { + schema: definitions["Error"]; + }; + /** Could not find product */ + 404: { + schema: definitions["Error"]; + }; + /** Unexpected error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + post: { + parameters: { + body: { + /** Plan create request */ + body: definitions["CreatePlan"]; + }; + }; + responses: { + /** Complete plan object */ + 201: { + schema: definitions["Plan"]; + }; + /** Invalid request provided */ + 400: { + schema: definitions["Error"]; + }; + /** Forbidden */ + 403: { + schema: definitions["Error"]; + }; + /** Plan already exists with that label */ + 409: { + schema: definitions["Error"]; + }; + /** Unexpected Error */ + 500: { + schema: definitions["Error"]; + }; + }; + }; + }; +} + +export interface definitions { + /** A base32 encoded 18 byte identifier. */ + ID: string; + /** A base32 encoded 18 byte identifier. */ + OptionalID: string; + /** A flexible identifier for internal or external entities. */ + FlexID: string; + /** A flexible identifier for internal or external entities. */ + OptionalFlexID: string; + /** A machine readable unique label, which is url safe. */ + Label: string; + /** A machine readable unique label, which is url safe. */ + OptionalLabel: string; + /** A machine readable unique label, which is url safe. */ + FeatureValueLabel: string; + /** A location of where a potential resource can be provisioned. */ + Location: string; + /** A name of a platform which is used to provision resources. */ + Platform: string; + /** A name of an entity which is displayed to a human. */ + Name: string; + /** A name of an entity which is displayed to a human. */ + OptionalName: string; + /** + * Logo used for Provider and Product listings. + * + * Must be square (same width and height) and minimum 400px. Maximum of 800px. + */ + LogoURL: string; + /** + * Logo used for Provider and Product listings. + * + * Must be square (same width and height) and minimum 400px. Maximum of 800px. + */ + OptionalLogoURL: string; + RegionBody: { + platform: definitions["Platform"]; + location: definitions["Location"]; + name: string; + priority: number; + }; + Region: { + id: definitions["ID"]; + type: "region"; + version: 1; + body: definitions["RegionBody"]; + }; + CreateRegion: { + body: definitions["RegionBody"]; + }; + UpdateRegion: { + name: string; + }; + ProviderBody: { + owner_id?: definitions["OptionalFlexID"]; + team_id?: definitions["OptionalID"]; + label: definitions["Label"]; + name: definitions["Name"]; + logo_url?: definitions["LogoURL"]; + support_email?: string; + documentation_url?: string; + }; + UpdateProviderBody: { + owner_id?: definitions["OptionalFlexID"]; + team_id?: definitions["OptionalID"]; + label?: definitions["OptionalLabel"]; + name?: definitions["OptionalName"]; + logo_url?: definitions["OptionalLogoURL"]; + support_email?: string; + documentation_url?: string; + }; + Provider: { + id: definitions["ID"]; + version: 1; + type: "provider"; + body: definitions["ProviderBody"]; + }; + CreateProvider: { + body: definitions["ProviderBody"]; + }; + UpdateProvider: { + id: definitions["ID"]; + body: definitions["UpdateProviderBody"]; + }; + UpdateProduct: { + id: definitions["ID"]; + body: definitions["UpdateProductBody"]; + }; + UpdateProductBody: { + name?: definitions["Name"]; + label?: definitions["Label"]; + logo_url?: definitions["LogoURL"]; + listing?: definitions["ProductListing"]; + /** 140 character sentence positioning the product. */ + tagline?: string; + /** A list of value propositions of the product. */ + value_props?: definitions["ValueProp"][]; + /** A list of getting started steps for the product */ + setup_steps?: string[]; + images?: definitions["ProductImageURL"][]; + support_email?: string; + documentation_url?: string; + /** + * URL to this Product's Terms of Service. If provided is true, then + * a url must be set. Otherwise, provided is false. + */ + terms_url?: string; + feature_types?: definitions["FeatureType"][]; + integration?: { + provisioning?: definitions["ProductProvisioning"]; + base_url?: string; + sso_url?: string; + version?: "v1"; + features?: { + access_code?: boolean; + sso?: boolean; + plan_change?: boolean; + credential?: "none" | "single" | "multiple" | "unknown"; + }; + }; + /** An array of platform ids to restrict this product for. */ + platform_ids?: definitions["ID"][]; + tags?: definitions["ProductTags"]; + }; + UpdatePlan: { + id: definitions["ID"]; + body: definitions["UpdatePlanBody"]; + }; + UpdatePlanBody: { + name?: definitions["Name"]; + label?: definitions["Label"]; + state?: definitions["PlanState"]; + /** Used in conjuction with resizable_to to set or unset the list */ + has_resize_constraints?: boolean; + resizable_to?: definitions["PlanResizeList"]; + /** Array of Region IDs */ + regions?: definitions["ID"][]; + /** Array of Feature Values */ + features?: definitions["FeatureValue"][]; + /** + * The number of days a user gets as a free trial when subscribing to + * this plan. Trials are valid only once per product; changing plans + * or adding an additional subscription will not start a new trial. + */ + trial_days?: number; + /** Dollar value in cents */ + cost?: number; + }; + /** + * A feature type represents the different aspects of a product that are + * offered, these features can manifest differently depending on the plan. + */ + FeatureType: { + label: definitions["Label"]; + name: definitions["Name"]; + type: "boolean" | "string" | "number"; + /** This sets whether or not the feature can be customized by a consumer. */ + customizable?: boolean; + /** + * This sets whether or not the feature can be upgraded by the consumer after the + * resource has provisioned. Upgrading means setting a higher value or selecting a + * higher element in the list. + */ + upgradable?: boolean; + /** + * This sets whether or not the feature can be downgraded by the consumer after the + * resource has provisioned. Downgrading means setting a lower value or selecting a + * lower element in the list. + */ + downgradable?: boolean; + /** + * Sets if this feature’s value is trackable from the provider, + * this only really affects numeric constraints. + */ + measurable?: boolean; + values?: definitions["FeatureValuesList"]; + }; + /** + * A list of allowable values for the feature. + * To define values for a boolean feature type, only `true` is required, + * using the label `true`, name and numeric_details will be ignored. + * If the feature is set measurable it is expected that these all have a + * `numeric_details` definition, and the plan will determine which + * `numeric_details` set is used based on it's setting. + */ + FeatureValuesList: definitions["FeatureValueDetails"][]; + FeatureValueDetails: { + label: definitions["FeatureValueLabel"]; + name: definitions["Name"]; + /** + * The cost that will be added to the monthly plan cost when this value + * is selected or is default for the plan. + * Cost is deprecated in favor of the `price.cost` field. + */ + cost?: number; + /** + * Price describes the cost of a feature. It should be preferred over + * the `cost` property. + */ + price?: { + /** + * Cost is the price in cents that will be added to plan's base cost + * when this value is selected or is default for the plan. + * Number features should use the cost range instead. + */ + cost?: number; + /** + * When a feature is used to multiply the cost of the plan or of + * another feature, multiply factor is used for calculation. + * A feature cannot have both a cost and a multiply factor. + */ + multiply_factor?: number; + /** Price describes how the feature cost should be calculated. */ + formula?: definitions["PriceFormula"]; + /** Description explains how a feature is calculated to the user. */ + description?: string; + }; + numeric_details?: definitions["FeatureNumericDetails"]; + }; + /** + * Optional container for additional details relating to numeric features. + * This is required if the feature is measurable and numeric. + */ + FeatureNumericDetails: { + /** + * Sets the increment at which numbers can be selected if customizable, by + * default this is 1; for example, setting this to 8 would only allow integers + * in increments of 8 ( 0, 8, 16, ... ). This property is not used if the + * feature is measurable; except if it is set to 0, setting the increment to 0 + * means this numeric details has no scale, and will not be or customizable. + * Some plans may not have a measureable or customizable feature. + */ + increment?: number; + /** Minimum value that can be set by a user if customizable */ + min?: number; + /** Maximum value that can be set by a user if customizable */ + max?: number; + /** Applied to the end of the number for display, for example the ‘GB’ in ‘20 GB’. */ + suffix?: string; + cost_ranges?: definitions["FeatureNumericRange"][]; + }; + FeatureNumericRange: { + /** + * Defines the end of the range ( inclusive ), from the previous, or 0; + * where the cost_multiple starts taking effect. If set to -1 this defines the + * range to infinity, or the maximum integer the system can handle + * ( whichever comes first ). + */ + limit?: number; + /** + * An integer in 10,000,000ths of cents, will be multiplied by the + * numeric value set in the feature to determine the cost. + */ + cost_multiple?: number; + }; + FeatureValue: { + feature: definitions["Label"]; + value: definitions["FeatureValueLabel"]; + }; + ValueProp: { + /** Heading of a value proposition. */ + header: string; + /** Body of a value proposition. */ + body: string; + }; + /** + * Image URL used for Product listings. + * + * Minimum 660px wide, 400px high. + */ + ProductImageURL: string; + /** List of tags for product categorization and search */ + ProductTags: definitions["Label"][]; + ProductState: "available" | "hidden" | "grandfathered" | "new" | "upcoming"; + ProductListing: { + /** + * When true, everyone can see the product when requested. When false it will + * not be visible to anyone except those on the provider team. + */ + public?: boolean; + /** + * When true, the product will be displayed in product listings alongside + * other products. When false the product will be excluded from listings, + * but can still be provisioned directly if it's label is known. + * Any pages that display information about the product when not listed, + * should indicate to webcrawlers that the content should not be indexed. + */ + listed?: boolean; + /** + * Object to hold various flags for marketing purposes only. These are values + * that need to be stored, but should not affect decision making in code. If + * we find ourselves in a position where we think they should, we should + * consider refactoring our listing definition. + */ + marketing?: { + /** + * Indicates whether or not the product is in `Beta` and should be + * advertised as such. This does not have any impact on who can access the + * product, it is just used to inform consumers through our clients. + */ + beta?: boolean; + /** + * Indicates whether or not the product is in `New` and should be + * advertised as such. This does not have any impact on who can access the + * product, it is just used to inform consumers through our clients. + */ + new?: boolean; + /** + * Indicates whether or not the product is in `New` and should be + * advertised as such. This does not have any impact on who can access the + * product, it is just used to inform consumers through our clients. + */ + featured?: boolean; + }; + }; + /** + * Provider Only, implies that the product should only be provisionable by the + * provider; so members of the provider team, no one else should be allowed. + * Pre-Order, should not be used yet. But in the future it should allow people to + * pre-provision a resource for when it does go live. + * Public, means the resource is live and everyone should be able to provision it. + */ + ProductProvisioning: "provider-only" | "pre-order" | "public"; + ProductIntegrationFeatures: { + /** + * Indicates whether or not this product supports resource transitions to + * manifold by access_code. + */ + access_code?: boolean; + /** + * Represents whether or not this product supports Single + * Sign On + */ + sso?: boolean; + /** + * Represents whether or not this product supports changing + * the plan of a resource. + */ + plan_change?: boolean; + /** + * Describes how the region for a resource is specified, if + * unspecified, then regions have no impact on this + * resource. + */ + region?: "user-specified" | "unspecified"; + /** + * Describes the credential type that is supported by this product. + * + * * `none`: The product does not support providing any credentials + * * `single`: Only one credential is supported at the same time. + * * `multiple`: Multiple credentials are supported at the same time. + * * `unknown`: The credential type is unknown. + */ + credential?: "none" | "single" | "multiple" | "unknown"; + }; + ProductBody: { + provider_id: definitions["ID"]; + /** Product labels are globally unique and contain the provider name. */ + label: definitions["Label"]; + name: definitions["Name"]; + state: definitions["ProductState"]; + listing: definitions["ProductListing"]; + logo_url: definitions["LogoURL"]; + /** 140 character sentence positioning the product. */ + tagline: string; + /** A list of value propositions of the product. */ + value_props: definitions["ValueProp"][]; + /** A list of getting started steps for the product */ + setup_steps?: string[]; + images: definitions["ProductImageURL"][]; + support_email: string; + documentation_url: string; + /** + * URL to this Product's Terms of Service. If provided is true, then + * a url must be set. Otherwise, provided is false. + */ + terms: { + url?: string; + provided: boolean; + }; + feature_types: definitions["FeatureType"][]; + billing: { + type: "monthly-prorated" | "monthly-anniversary" | "annual-anniversary"; + currency: "usd"; + }; + integration: { + provisioning: definitions["ProductProvisioning"]; + base_url: string; + sso_url?: string; + version: "v1"; + features: definitions["ProductIntegrationFeatures"]; + }; + tags?: definitions["ProductTags"]; + }; + Product: { + id: definitions["ID"]; + version: 1; + type: "product"; + body: definitions["ProductBody"]; + }; + CreateProduct: { + body: definitions["ProductBody"]; + }; + /** Array of Plan IDs that this Plan can be resized to, if null all will be assumed */ + PlanResizeList: definitions["ID"][]; + PlanBody: { + provider_id: definitions["ID"]; + product_id: definitions["ID"]; + name: definitions["Name"]; + label: definitions["Label"]; + state: definitions["PlanState"]; + resizable_to?: definitions["PlanResizeList"]; + /** Array of Region IDs */ + regions: definitions["ID"][]; + /** Array of Feature Values */ + features: definitions["FeatureValue"][]; + /** + * The number of days a user gets as a free trial when subscribing to + * this plan. Trials are valid only once per product; changing plans + * or adding an additional subscription will not start a new trial. + */ + trial_days?: number; + /** Dollar value in cents. */ + cost: number; + }; + PlanState: "hidden" | "available" | "grandfathered" | "unlisted"; + ExpandedPlanBody: definitions["PlanBody"] & { + /** An array of feature definitions for the plan, as defined on the Product. */ + expanded_features: definitions["ExpandedFeature"][]; + /** A boolean flag that indicates if a plan is free or not based on it's cost and features. */ + free: boolean; + /** Plan cost using its default features plus base cost. */ + defaultCost?: number; + /** A boolean flag that indicates if a plan has customizable features. */ + customizable?: boolean; + }; + ExpandedFeature: definitions["FeatureType"] & { + /** The string value set for the feature on the plan, this should only be used if the value property is null. */ + value_string: string; + value: definitions["FeatureValueDetails"]; + }; + Plan: { + id: definitions["ID"]; + version: 1; + type: "plan"; + body: definitions["PlanBody"]; + }; + ExpandedPlan: { + id: definitions["ID"]; + version: 1; + type: "plan"; + body: definitions["ExpandedPlanBody"]; + }; + CreatePlan: { + body: definitions["PlanBody"]; + }; + /** Unexpected error */ + Error: { + /** The error type */ + type: string; + /** Explanation of the errors */ + message: string[]; + }; + /** + * Describes how a feature cost should be calculated. An empty + * string defaults to the normal price calculation using the value cost. + * Formula uses Reverse Polish notation for statements. It supports + * addition, subtraction and multiplication operations. Operations must be + * grouped with parenthesis. + * Number literals can be used for formulas. Eg: "(- feature-a#cost 500)" + * will remove 5 dollars from the cost of feature a. + * Multiplication operation supports either a cost multiplied by a + * factor or a number multiplied by a factor. + * In a plan formula the following keywords are available: + * - `plan#base_cost` is the base cost of a plan in cents + * - `plan#partial_cost` is the base cost plus its feature costs calculated + * so far. Feature formulas are calculated in the order they are defined, + * so features can refer to another feature values or the partial_cost of + * the plan. + * - `this-feature-label#multiply_factor` is the multiply_factor of this + * feature as a float number. + * - `another-feature-label#cost` is the cost of a feature matching the label + * in cents. + * - `another-feature-label#number` is the numeric value of a number feature + * In a feature formula, plan base cost and total cost cannot be used + */ + PriceFormula: string; + ExpandedProduct: { + id: definitions["ID"]; + version: 1; + type: "product"; + body: definitions["ProductBody"]; + plans?: definitions["ExpandedPlan"][]; + provider: definitions["Provider"]; + }; +} + +export interface parameters { + /** Filter results to only include those that have this label. */ + LabelFilter: string; +} + +export interface operations {} diff --git a/tests/bin/expected/petstore.ts b/tests/bin/expected/petstore.ts new file mode 100644 index 000000000..59460406a --- /dev/null +++ b/tests/bin/expected/petstore.ts @@ -0,0 +1,465 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/pet": { + put: operations["updatePet"]; + post: operations["addPet"]; + }; + "/pet/findByStatus": { + /** Multiple status values can be provided with comma separated strings */ + get: operations["findPetsByStatus"]; + }; + "/pet/findByTags": { + /** Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ + get: operations["findPetsByTags"]; + }; + "/pet/{petId}": { + /** Returns a single pet */ + get: operations["getPetById"]; + post: operations["updatePetWithForm"]; + delete: operations["deletePet"]; + }; + "/pet/{petId}/uploadImage": { + post: operations["uploadFile"]; + }; + "/store/inventory": { + /** Returns a map of status codes to quantities */ + get: operations["getInventory"]; + }; + "/store/order": { + post: operations["placeOrder"]; + }; + "/store/order/{orderId}": { + /** For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions */ + get: operations["getOrderById"]; + /** For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors */ + delete: operations["deleteOrder"]; + }; + "/user": { + /** This can only be done by the logged in user. */ + post: operations["createUser"]; + }; + "/user/createWithArray": { + post: operations["createUsersWithArrayInput"]; + }; + "/user/createWithList": { + post: operations["createUsersWithListInput"]; + }; + "/user/login": { + get: operations["loginUser"]; + }; + "/user/logout": { + get: operations["logoutUser"]; + }; + "/user/{username}": { + get: operations["getUserByName"]; + /** This can only be done by the logged in user. */ + put: operations["updateUser"]; + /** This can only be done by the logged in user. */ + delete: operations["deleteUser"]; + }; +} + +export interface components { + schemas: { + Order: { + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + /** Order Status */ + status?: "placed" | "approved" | "delivered"; + complete?: boolean; + } & { [key: string]: any }; + Category: { + id?: number; + name?: string; + } & { [key: string]: any }; + User: { + id?: number; + username?: string; + firstName?: string; + lastName?: string; + email?: string; + password?: string; + phone?: string; + /** User Status */ + userStatus?: number; + } & { [key: string]: any }; + Tag: { + id?: number; + name?: string; + } & { [key: string]: any }; + Pet: { + id?: number; + category?: components["schemas"]["Category"]; + name: string; + photoUrls: string[]; + tags?: components["schemas"]["Tag"][]; + /** pet status in the store */ + status?: "available" | "pending" | "sold"; + } & { [key: string]: any }; + ApiResponse: { + code?: number; + type?: string; + message?: string; + } & { [key: string]: any }; + }; +} + +export interface operations { + updatePet: { + responses: { + /** Invalid ID supplied */ + 400: unknown; + /** Pet not found */ + 404: unknown; + /** Validation exception */ + 405: unknown; + }; + /** Pet object that needs to be added to the store */ + requestBody: { + content: { + "application/json": components["schemas"]["Pet"]; + "application/xml": components["schemas"]["Pet"]; + }; + }; + }; + addPet: { + responses: { + /** Invalid input */ + 405: unknown; + }; + /** Pet object that needs to be added to the store */ + requestBody: { + content: { + "application/json": components["schemas"]["Pet"]; + "application/xml": components["schemas"]["Pet"]; + }; + }; + }; + /** Multiple status values can be provided with comma separated strings */ + findPetsByStatus: { + parameters: { + query: { + /** Status values that need to be considered for filter */ + status: ("available" | "pending" | "sold")[]; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Pet"][]; + "application/json": components["schemas"]["Pet"][]; + }; + }; + /** Invalid status value */ + 400: unknown; + }; + }; + /** Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ + findPetsByTags: { + parameters: { + query: { + /** Tags to filter by */ + tags: string[]; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Pet"][]; + "application/json": components["schemas"]["Pet"][]; + }; + }; + /** Invalid tag value */ + 400: unknown; + }; + }; + /** Returns a single pet */ + getPetById: { + parameters: { + path: { + /** ID of pet to return */ + petId: number; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Pet"]; + "application/json": components["schemas"]["Pet"]; + }; + }; + /** Invalid ID supplied */ + 400: unknown; + /** Pet not found */ + 404: unknown; + }; + }; + updatePetWithForm: { + parameters: { + path: { + /** ID of pet that needs to be updated */ + petId: number; + }; + }; + responses: { + /** Invalid input */ + 405: unknown; + }; + requestBody: { + content: { + "application/x-www-form-urlencoded": { + /** Updated name of the pet */ + name?: string; + /** Updated status of the pet */ + status?: string; + } & { [key: string]: any }; + }; + }; + }; + deletePet: { + parameters: { + header: { + api_key?: string; + }; + path: { + /** Pet id to delete */ + petId: number; + }; + }; + responses: { + /** Invalid ID supplied */ + 400: unknown; + /** Pet not found */ + 404: unknown; + }; + }; + uploadFile: { + parameters: { + path: { + /** ID of pet to update */ + petId: number; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/json": components["schemas"]["ApiResponse"]; + }; + }; + }; + requestBody: { + content: { + "multipart/form-data": { + /** Additional data to pass to server */ + additionalMetadata?: string; + /** file to upload */ + file?: string; + } & { [key: string]: any }; + }; + }; + }; + /** Returns a map of status codes to quantities */ + getInventory: { + responses: { + /** successful operation */ + 200: { + content: { + "application/json": { [key: string]: number }; + }; + }; + }; + }; + placeOrder: { + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Order"]; + "application/json": components["schemas"]["Order"]; + }; + }; + /** Invalid Order */ + 400: unknown; + }; + /** order placed for purchasing the pet */ + requestBody: { + content: { + "*/*": components["schemas"]["Order"]; + }; + }; + }; + /** For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions */ + getOrderById: { + parameters: { + path: { + /** ID of pet that needs to be fetched */ + orderId: number; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["Order"]; + "application/json": components["schemas"]["Order"]; + }; + }; + /** Invalid ID supplied */ + 400: unknown; + /** Order not found */ + 404: unknown; + }; + }; + /** For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors */ + deleteOrder: { + parameters: { + path: { + /** ID of the order that needs to be deleted */ + orderId: number; + }; + }; + responses: { + /** Invalid ID supplied */ + 400: unknown; + /** Order not found */ + 404: unknown; + }; + }; + /** This can only be done by the logged in user. */ + createUser: { + responses: { + /** successful operation */ + default: unknown; + }; + /** Created user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"]; + }; + }; + }; + createUsersWithArrayInput: { + responses: { + /** successful operation */ + default: unknown; + }; + /** List of user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"][]; + }; + }; + }; + createUsersWithListInput: { + responses: { + /** successful operation */ + default: unknown; + }; + /** List of user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"][]; + }; + }; + }; + loginUser: { + parameters: { + query: { + /** The user name for login */ + username: string; + /** The password for login in clear text */ + password: string; + }; + }; + responses: { + /** successful operation */ + 200: { + headers: { + /** calls per hour allowed by the user */ + "X-Rate-Limit"?: number; + /** date in UTC when token expires */ + "X-Expires-After"?: string; + }; + content: { + "application/xml": string; + "application/json": string; + }; + }; + /** Invalid username/password supplied */ + 400: unknown; + }; + }; + logoutUser: { + responses: { + /** successful operation */ + default: unknown; + }; + }; + getUserByName: { + parameters: { + path: { + /** The name that needs to be fetched. Use user1 for testing. */ + username: string; + }; + }; + responses: { + /** successful operation */ + 200: { + content: { + "application/xml": components["schemas"]["User"]; + "application/json": components["schemas"]["User"]; + }; + }; + /** Invalid username supplied */ + 400: unknown; + /** User not found */ + 404: unknown; + }; + }; + /** This can only be done by the logged in user. */ + updateUser: { + parameters: { + path: { + /** name that need to be updated */ + username: string; + }; + }; + responses: { + /** Invalid user supplied */ + 400: unknown; + /** User not found */ + 404: unknown; + }; + /** Updated user object */ + requestBody: { + content: { + "*/*": components["schemas"]["User"]; + }; + }; + }; + /** This can only be done by the logged in user. */ + deleteUser: { + parameters: { + path: { + /** The name that needs to be deleted */ + username: string; + }; + }; + responses: { + /** Invalid username supplied */ + 400: unknown; + /** User not found */ + 404: unknown; + }; + }; +} diff --git a/tests/bin/specs/manifold.yaml b/tests/bin/specs/manifold.yaml new file mode 100644 index 000000000..a6bde2da3 --- /dev/null +++ b/tests/bin/specs/manifold.yaml @@ -0,0 +1,1674 @@ +swagger: '2.0' +info: + title: Catalog API + description: Access Regions, Providers, Products and Plans + version: '1.0.0' + +# the domain of the service +host: api.catalog.manifold.co +schemes: [https] +produces: [application/json] +consumes: [application/json] + +securityDefinitions: + tokenRequired: + type: apiKey + name: Authorization + in: header + xInternalAuthRequired: + x-manifold-audience: internal + type: apiKey + name: X-Internal-Auth + in: header + anonymous: + # Hack to allow for an unauthenticated security definition. + type: apiKey + name: Manifold-Anonymous + in: header + +basePath: /v1 +paths: + /regions/: + get: + summary: List all available regions + tags: + - Region + parameters: + - name: location + in: query + description: | + Filter results to only include the regions that have this location. + type: string + format: label + pattern: '^[a-z0-9][a-z0-9\-\_]{1,128}$' + - name: platform + in: query + description: | + Filter results to only include the regions that are on this + platform. + type: string + format: label + pattern: '^[a-z0-9][a-z0-9\-\_]{1,128}$' + responses: + 200: + description: A list of regions. + schema: + type: array + items: { $ref: '#/definitions/Region' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + post: + security: + - { xInternalAuthRequired: [] } + summary: Add a new region + tags: + - Region + parameters: + - name: body + in: body + description: Region create request + required: true + schema: { $ref: '#/definitions/CreateRegion' } + responses: + 201: + description: Complete region object + schema: { $ref: '#/definitions/Region' } + 400: + description: Invalid request provided + schema: { $ref: '#/definitions/Error' } + 409: + description: Region already exists for that platform and location + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + /regions/{id}: + get: + summary: Get a Region by ID + parameters: + - name: id + in: path + description: ID of the region to lookup, stored as a base32 encoded 18 byte identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + tags: + - Region + responses: + 200: + description: A region. + schema: { $ref: '#/definitions/Region' } + 400: + description: Provided Region ID is Invalid + schema: { $ref: '#/definitions/Error' } + 404: + description: Region could not be found + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + patch: + security: + - { xInternalAuthRequired: [] } + summary: Patches an existing region + tags: + - Region + parameters: + - name: id + in: path + description: ID of the region to lookup, stored as a base32 encoded 18 byte identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + - name: body + in: body + description: Region update request + required: true + schema: { $ref: '#/definitions/UpdateRegion' } + responses: + 200: + description: Complete region object + schema: { $ref: '#/definitions/Region' } + 400: + description: Invalid request provided + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + + /providers/: + get: + summary: List all available providers + tags: + - Provider + parameters: + - { $ref: '#/parameters/LabelFilter' } + responses: + 200: + description: A list of providers. + schema: + type: array + items: { $ref: '#/definitions/Provider' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + post: + security: + - { xInternalAuthRequired: [] } + summary: Add a new provider + tags: + - Provider + parameters: + - name: body + in: body + description: Provider create request + required: true + schema: { $ref: '#/definitions/CreateProvider' } + responses: + 201: + description: Complete provider object + schema: { $ref: '#/definitions/Provider' } + 400: + description: Invalid request provided + schema: { $ref: '#/definitions/Error' } + 403: + description: Forbidden + schema: { $ref: '#/definitions/Error' } + 409: + description: Provider already exists with that label + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + /providers/{id}: + get: + summary: Get a provider by ID + parameters: + - name: id + in: path + description: ID of the provider to lookup, stored as a base32 encoded 18 byte identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + tags: + - Provider + responses: + 200: + description: A provider. + schema: { $ref: '#/definitions/Provider' } + 404: + description: Unknown provider error + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + patch: + security: + - { xInternalAuthRequired: [] } + summary: Update a provider + parameters: + - name: id + in: path + description: ID of the provider to update, stored as a base32 encoded 18 byte identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + - name: body + in: body + description: Provider update request + required: true + schema: { $ref: '#/definitions/UpdateProvider' } + tags: + - Provider + responses: + 200: + description: Complete provider object + schema: { $ref: '#/definitions/Provider' } + 400: + description: Invalid request provided + schema: { $ref: '#/definitions/Error' } + 403: + description: Forbidden + schema: { $ref: '#/definitions/Error' } + 404: + description: Provider not found + schema: { $ref: '#/definitions/Error' } + 409: + description: Provider already exists with that label + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + /products/: + get: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + - { anonymous: [] } + summary: List all available products + parameters: + - name: provider_id + in: query + description: | + Base32 encoded 18 byte identifier of the provider that these + products must belong to. + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + - { $ref: '#/parameters/LabelFilter' } + - name: tags + in: query + description: Return only products matching at least one of the tags. + type: array + collectionFormat: csv + items: + type: string + format: label + pattern: '^[a-z0-9][a-z0-9\-\_]{1,128}$' + tags: + - Product + responses: + 200: + description: A product. + schema: + type: array + items: { $ref: '#/definitions/Product' } + 400: + description: Invalid provider_id supplied + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + post: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + summary: Add a new product + tags: + - Product + parameters: + - name: body + in: body + description: Product create request + required: true + schema: { $ref: '#/definitions/CreateProduct' } + responses: + 201: + description: Complete product object + schema: { $ref: '#/definitions/Product' } + 400: + description: Invalid request provided + schema: { $ref: '#/definitions/Error' } + 403: + description: Forbidden + schema: { $ref: '#/definitions/Error' } + 409: + description: Product already exists with that label + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + /internal/products: + get: + security: + - { tokenRequired: [] } + - { anonymous: [] } + summary: Get products and associated information + parameters: + - name: provider_id + in: query + description: | + Base32 encoded 18 byte identifier of the provider that these + products must belong to. + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + - { $ref: '#/parameters/LabelFilter' } + - name: tags + in: query + description: Return only products matching at least one of the tags. + type: array + collectionFormat: csv + items: + type: string + format: label + pattern: '^[a-z0-9][a-z0-9\-\_]{1,128}$' + - name: include_plans + in: query + description: Return product listings without plan information + type: boolean + default: true + tags: + - Product + responses: + 200: + description: A product. + schema: + type: array + items: { $ref: '#/definitions/ExpandedProduct' } + 400: + description: Invalid provider_id supplied + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + /products/{id}: + get: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + - { anonymous: [] } + summary: Get a product by ID + parameters: + - name: id + in: path + description: | + ID of the product to lookup, stored as a base32 encoded 18 byte + identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + tags: + - Product + responses: + 200: + description: A product. + schema: { $ref: '#/definitions/Product' } + 400: + description: Invalid Product ID + schema: { $ref: '#/definitions/Error' } + 404: + description: Product not found error + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected error + schema: { $ref: '#/definitions/Error' } + patch: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + summary: Update a product + parameters: + - name: id + in: path + description: | + ID of the product to lookup, stored as a base32 encoded 18 byte + identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + - name: body + in: body + description: Product update request + required: true + schema: { $ref: '#/definitions/UpdateProduct' } + tags: + - Product + responses: + 200: + description: Complete product object + schema: { $ref: '#/definitions/Product' } + 400: + description: Invalid Product ID + schema: { $ref: '#/definitions/Error' } + 404: + description: Product not found error + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected error + schema: { $ref: '#/definitions/Error' } + /plans/{id}: + get: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + - { anonymous: [] } + summary: Get a plan by ID + parameters: + - name: id + in: path + description: | + ID of the plan to lookup, stored as a base32 encoded 18 byte + identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + tags: + - Plan + responses: + 200: + description: A plan. + schema: { $ref: '#/definitions/ExpandedPlan' } + 400: + description: Invalid Plan ID Provided + schema: { $ref: '#/definitions/Error' } + 404: + description: Unknown plan error + schema: { $ref: '#/definitions/Error' } + default: + description: Unexpected error + schema: { $ref: '#/definitions/Error' } + patch: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + summary: Update a plan + parameters: + - name: id + in: path + description: | + ID of the plan to lookup, stored as a base32 encoded 18 byte + identifier. + required: true + type: string + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + - name: body + in: body + description: Plan update request + required: true + schema: { $ref: '#/definitions/UpdatePlan' } + tags: + - Plan + responses: + 200: + description: Complete product plan + schema: { $ref: '#/definitions/Plan' } + 400: + description: Invalid Plan ID + schema: { $ref: '#/definitions/Error' } + 404: + description: Plan not found error + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected error + schema: { $ref: '#/definitions/Error' } + /plans/: + get: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + - { anonymous: [] } + summary: Get a list of plans. + parameters: + - name: product_id + in: query + description: Return the plans that are associated with this product. + required: true + type: array + collectionFormat: 'multi' + items: + format: base32ID + type: string + - { $ref: '#/parameters/LabelFilter' } + tags: + - Plan + responses: + 200: + description: A list of plans for the given product. + schema: + type: array + items: { $ref: '#/definitions/ExpandedPlan' } + 400: + description: Invalid Parameters Provided + schema: { $ref: '#/definitions/Error' } + 404: + description: Could not find product + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected error + schema: { $ref: '#/definitions/Error' } + post: + security: + - { xInternalAuthRequired: [] } + - { tokenRequired: [] } + summary: Add a new plan + tags: + - Plan + parameters: + - name: body + in: body + description: Plan create request + required: true + schema: { $ref: '#/definitions/CreatePlan' } + responses: + 201: + description: Complete plan object + schema: { $ref: '#/definitions/Plan' } + 400: + description: Invalid request provided + schema: { $ref: '#/definitions/Error' } + 403: + description: Forbidden + schema: { $ref: '#/definitions/Error' } + 409: + description: Plan already exists with that label + schema: { $ref: '#/definitions/Error' } + 500: + description: Unexpected Error + schema: { $ref: '#/definitions/Error' } + +parameters: + LabelFilter: + name: label + in: query + description: | + Filter results to only include those that have this label. + type: string + format: label + pattern: '^[a-z0-9][a-z0-9\-\_]{1,128}$' + +definitions: + ID: + type: string + description: A base32 encoded 18 byte identifier. + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + x-go-type: + type: ID + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + OptionalID: + type: string + description: A base32 encoded 18 byte identifier. + pattern: '^[0-9abcdefghjkmnpqrtuvwxyz]{29}$' + format: base32ID + x-nullable: true + x-go-type: + type: ID + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + FlexID: + type: string + description: A flexible identifier for internal or external entities. + pattern: '^((?:[a-zA-Z0-9-_]+\.)*)[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,11}?\/[a-z0-9][a-z0-9-_]{1,128}\/\{?[a-zA-Z0-9-_]{1,256}={0,2}\}?$' + x-go-type: + type: FlexID + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + OptionalFlexID: + type: string + description: A flexible identifier for internal or external entities. + pattern: '^((?:[a-zA-Z0-9-_]+\.)*)[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,11}?\/[a-z0-9][a-z0-9-_]{1,128}\/\{?[a-zA-Z0-9-_]{1,256}={0,2}\}?$' + x-nullable: true + x-go-type: + type: FlexID + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + + Label: + type: string + description: A machine readable unique label, which is url safe. + pattern: '^[a-z0-9][a-z0-9\-\_]{1,128}$' + x-go-type: + type: Label + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + OptionalLabel: + type: string + description: A machine readable unique label, which is url safe. + pattern: '^[a-z0-9][a-z0-9\-\_]{1,128}$' + x-nullable: true + x-go-type: + type: Label + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + + FeatureValueLabel: + type: string + description: A machine readable unique label, which is url safe. + pattern: '^[a-z0-9][a-z0-9-_\.]{1,128}$' + x-go-type: + type: FeatureValueLabel + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + + Location: + type: string + description: A location of where a potential resource can be provisioned. + pattern: '^[a-z0-9][a-z0-9\-]{1,128}$' + + Platform: + type: string + description: A name of a platform which is used to provision resources. + pattern: '^[a-z0-9][a-z0-9\-]{1,128}$' + + Name: + type: string + description: A name of an entity which is displayed to a human. + pattern: '^[a-zA-Z0-9][a-z0-9A-Z\. \-_]{2,128}$' + x-go-type: + type: Name + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + OptionalName: + type: string + description: A name of an entity which is displayed to a human. + pattern: '^[a-zA-Z0-9][a-z0-9A-Z\. \-_]{2,128}$' + x-nullable: true + x-go-type: + type: Name + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + + LogoURL: + type: string + description: | + Logo used for Provider and Product listings. + + Must be square (same width and height) and minimum 400px. Maximum of 800px. + format: url + pattern: '^https:\/\/cdn\.(?:stage\.)?manifold.co' + OptionalLogoURL: + type: string + description: | + Logo used for Provider and Product listings. + + Must be square (same width and height) and minimum 400px. Maximum of 800px. + format: url + pattern: '^https:\/\/cdn\.(?:stage\.)?manifold.co' + x-nullable: true + + RegionBody: + type: object + properties: + platform: { $ref: '#/definitions/Platform' } + location: { $ref: '#/definitions/Location' } + name: { type: string, x-nullable: false } + priority: + type: number + multipleOf: 1 + minimum: 0 + maximum: 100 + x-nullable: false + additionalProperties: false + required: + - platform + - location + - name + - priority + + Region: + type: object + properties: + id: { $ref: '#/definitions/ID' } + type: { type: string, enum: [region] } + version: { type: integer, enum: [1] } + body: { $ref: '#/definitions/RegionBody' } + additionalProperties: false + required: + - id + - type + - version + - body + + CreateRegion: + type: object + properties: + body: { $ref: '#/definitions/RegionBody' } + additionalProperties: false + required: + - body + + UpdateRegion: + type: object + properties: + name: { type: string } + additionalProperties: false + required: + - name + + ProviderBody: + type: object + properties: + owner_id: { $ref: '#/definitions/OptionalFlexID' } + team_id: { $ref: '#/definitions/OptionalID' } + label: { $ref: '#/definitions/Label' } + name: { $ref: '#/definitions/Name' } + logo_url: { $ref: '#/definitions/LogoURL' } + support_email: + type: string + format: email + documentation_url: + type: string + format: url + additionalProperties: false + required: + - label + - name + + UpdateProviderBody: + type: object + properties: + owner_id: { $ref: '#/definitions/OptionalFlexID' } + team_id: { $ref: '#/definitions/OptionalID' } # deprecated in favor of OwnerID + label: { $ref: '#/definitions/OptionalLabel' } + name: { $ref: '#/definitions/OptionalName' } + logo_url: { $ref: '#/definitions/OptionalLogoURL' } + support_email: + type: string + format: email + x-nullable: true + documentation_url: + type: string + format: url + x-nullable: true + additionalProperties: false + + Provider: + type: object + properties: + id: { $ref: '#/definitions/ID' } + version: { type: integer, enum: [1] } + type: { type: string, enum: ['provider'] } + body: { $ref: '#/definitions/ProviderBody' } + additionalProperties: false + required: + - id + - version + - type + - body + + CreateProvider: + type: object + properties: + body: { $ref: '#/definitions/ProviderBody' } + additionalProperties: false + required: + - body + + UpdateProvider: + type: object + properties: + id: { $ref: '#/definitions/ID' } + body: { $ref: '#/definitions/UpdateProviderBody' } + additionalProperties: false + required: + - id + - body + + UpdateProduct: + type: object + properties: + id: { $ref: '#/definitions/ID' } + body: { $ref: '#/definitions/UpdateProductBody' } + additionalProperties: false + required: + - id + - body + + UpdateProductBody: + type: object + properties: + name: + $ref: '#/definitions/Name' + x-nullable: true + label: + $ref: '#/definitions/Label' + x-nullable: true + logo_url: + $ref: '#/definitions/LogoURL' + x-nullable: true + listing: + $ref: '#/definitions/ProductListing' + x-nullable: true + tagline: + description: | + 140 character sentence positioning the product. + type: string + maxLength: 140 + x-nullable: true + value_props: + description: A list of value propositions of the product. + type: array + items: { $ref: '#/definitions/ValueProp' } + maxItems: 8 + x-nullable: true + setup_steps: + description: A list of getting started steps for the product + type: array + items: + description: A markdown string describing a step in how to setup a product + type: string + minLength: 3 + maxLength: 200 + maxItems: 8 + x-nullable: true + images: + type: array + items: { $ref: '#/definitions/ProductImageURL' } + maxItems: 8 + x-nullable: true + support_email: + type: string + format: email + x-nullable: true + documentation_url: + type: string + format: url + x-nullable: true + terms_url: + description: | + URL to this Product's Terms of Service. If provided is true, then + a url must be set. Otherwise, provided is false. + type: string + x-nullable: true + feature_types: + type: array + items: { $ref: '#/definitions/FeatureType' } + x-nullable: true + integration: + type: object + properties: + provisioning: + $ref: '#/definitions/ProductProvisioning' + x-nullable: true + base_url: + type: string + format: url + x-nullable: true + sso_url: + type: string + format: url + x-nullable: true + version: + type: string + enum: ['v1'] + x-nullable: true + features: + type: object + properties: + access_code: + type: boolean + x-nullable: true + sso: + type: boolean + x-nullable: true + plan_change: + type: boolean + x-nullable: true + credential: + type: string + default: 'multiple' + enum: + - 'none' + - 'single' + - 'multiple' + - 'unknown' + x-nullable: true + additionalProperties: false + default: {} + additionalProperties: false + x-nullable: true + platform_ids: + type: array + description: > + An array of platform ids to restrict this product for. + items: { $ref: '#/definitions/ID' } + x-nullable: true + tags: { $ref: '#/definitions/ProductTags' } + additionalProperties: false + + UpdatePlan: + type: object + properties: + id: { $ref: '#/definitions/ID' } + body: { $ref: '#/definitions/UpdatePlanBody' } + additionalProperties: false + required: + - id + - body + + UpdatePlanBody: + type: object + properties: + name: { $ref: '#/definitions/Name' } + label: { $ref: '#/definitions/Label' } + state: { $ref: '#/definitions/PlanState' } + has_resize_constraints: + type: boolean + description: Used in conjuction with resizable_to to set or unset the list + x-nullable: true + resizable_to: { $ref: '#/definitions/PlanResizeList' } + regions: + type: array + description: 'Array of Region IDs' + items: { $ref: '#/definitions/ID' } + x-nullable: true + features: + type: array + description: 'Array of Feature Values' + items: { $ref: '#/definitions/FeatureValue' } + x-nullable: true + trial_days: + type: integer + minimum: 0 + description: | + The number of days a user gets as a free trial when subscribing to + this plan. Trials are valid only once per product; changing plans + or adding an additional subscription will not start a new trial. + x-nullable: true + cost: + type: integer + minimum: 0 + description: Dollar value in cents + x-nullable: true + additionalProperties: false + + FeatureType: + description: | + A feature type represents the different aspects of a product that are + offered, these features can manifest differently depending on the plan. + type: object + properties: + label: { $ref: '#/definitions/Label' } + name: { $ref: '#/definitions/Name' } + type: + type: string + enum: + - boolean + - string + - number + customizable: + type: boolean + default: false + description: This sets whether or not the feature can be customized by a consumer. + x-nullable: false + upgradable: + type: boolean + default: false + description: | + This sets whether or not the feature can be upgraded by the consumer after the + resource has provisioned. Upgrading means setting a higher value or selecting a + higher element in the list. + x-nullable: false + downgradable: + type: boolean + default: false + description: | + This sets whether or not the feature can be downgraded by the consumer after the + resource has provisioned. Downgrading means setting a lower value or selecting a + lower element in the list. + x-nullable: false + measurable: + type: boolean + default: false + description: | + Sets if this feature’s value is trackable from the provider, + this only really affects numeric constraints. + x-nullable: false + values: { $ref: '#/definitions/FeatureValuesList' } + additionalProperties: false + required: + - label + - name + - type + + FeatureValuesList: + type: array + description: | + A list of allowable values for the feature. + To define values for a boolean feature type, only `true` is required, + using the label `true`, name and numeric_details will be ignored. + If the feature is set measurable it is expected that these all have a + `numeric_details` definition, and the plan will determine which + `numeric_details` set is used based on it's setting. + items: { $ref: '#/definitions/FeatureValueDetails' } + x-nullable: true + + FeatureValueDetails: + type: object + properties: + label: { $ref: '#/definitions/FeatureValueLabel' } + name: { $ref: '#/definitions/Name' } + cost: + type: integer + minimum: 0 + default: 0 + description: | + The cost that will be added to the monthly plan cost when this value + is selected or is default for the plan. + Cost is deprecated in favor of the `price.cost` field. + x-nullable: false + price: + type: object + properties: + cost: + type: integer + minimum: 0 + default: 0 + x-nullable: false + description: | + Cost is the price in cents that will be added to plan's base cost + when this value is selected or is default for the plan. + Number features should use the cost range instead. + multiply_factor: + type: number + minimum: 0 + default: 0 + x-nullable: false + description: | + When a feature is used to multiply the cost of the plan or of + another feature, multiply factor is used for calculation. + A feature cannot have both a cost and a multiply factor. + formula: + $ref: '#/definitions/PriceFormula' + description: | + Price describes how the feature cost should be calculated. + example: + feature_multiplies_base_cost: '(* plan#base_cost feature-a#multiply_factor)' + feature_multiplies_feature_cost: '(* feature-b#cost feature-a#multiply_factor)' + feature_multiplies_numeric_value: '(* feature-c#number feature-a#multiply_factor)' + feature_multiplies_total_cost: '(* plan#total_cost feature-a#multiply_factor)' + feature_nested_formulas: + '(+ (- (* feature-a#cost feature-b#multiply_factor) 500) plan#partial_cost)' + description: + type: string + description: | + Description explains how a feature is calculated to the user. + description: | + Price describes the cost of a feature. It should be preferred over + the `cost` property. + additionalProperties: false + numeric_details: { $ref: '#/definitions/FeatureNumericDetails' } + additionalProperties: false + required: + - label + - name + + FeatureNumericDetails: + type: object + description: | + Optional container for additional details relating to numeric features. + This is required if the feature is measurable and numeric. + properties: + increment: + type: integer + minimum: 0 + default: 1 + description: | + Sets the increment at which numbers can be selected if customizable, by + default this is 1; for example, setting this to 8 would only allow integers + in increments of 8 ( 0, 8, 16, ... ). This property is not used if the + feature is measurable; except if it is set to 0, setting the increment to 0 + means this numeric details has no scale, and will not be or customizable. + Some plans may not have a measureable or customizable feature. + x-nullable: false + min: + type: integer + default: 0 + minimum: 0 + description: Minimum value that can be set by a user if customizable + x-nullable: false + max: + type: integer + minimum: 1 + description: Maximum value that can be set by a user if customizable + x-nullable: true + suffix: + type: string + description: Applied to the end of the number for display, for example the ‘GB’ in ‘20 GB’. + x-nullable: true + cost_ranges: + type: array + items: { $ref: '#/definitions/FeatureNumericRange' } + maxItems: 15 + x-nullable: true + x-nullable: true + + FeatureNumericRange: + type: object + properties: + limit: + type: integer + minimum: -1 + description: | + Defines the end of the range ( inclusive ), from the previous, or 0; + where the cost_multiple starts taking effect. If set to -1 this defines the + range to infinity, or the maximum integer the system can handle + ( whichever comes first ). + x-nullable: false + cost_multiple: + type: integer + default: 0 + minimum: 0 + description: | + An integer in 10,000,000ths of cents, will be multiplied by the + numeric value set in the feature to determine the cost. + x-nullable: false + + FeatureValue: + type: object + properties: + feature: { $ref: '#/definitions/Label' } + value: { $ref: '#/definitions/FeatureValueLabel' } + additionalProperties: false + required: + - feature + - value + + ValueProp: + type: object + properties: + header: + description: Heading of a value proposition. + type: string + minLength: 3 + maxLength: 80 + x-nullable: false + body: + description: Body of a value proposition. + type: string + minLength: 10 + maxLength: 500 + x-nullable: false + additionalProperties: false + required: + - header + - body + + ProductImageURL: + type: string + description: | + Image URL used for Product listings. + + Minimum 660px wide, 400px high. + format: url + pattern: '^https:\/\/cdn\.(?:stage\.)?manifold.co' + + ProductTags: + type: array + description: 'List of tags for product categorization and search' + items: { $ref: '#/definitions/Label' } + maxItems: 10 + + ProductState: + type: string + enum: + - 'available' + - 'hidden' + - 'grandfathered' + - 'new' + - 'upcoming' + + ProductListing: + type: object + properties: + public: + type: boolean + default: false + description: | + When true, everyone can see the product when requested. When false it will + not be visible to anyone except those on the provider team. + listed: + type: boolean + default: false + description: | + When true, the product will be displayed in product listings alongside + other products. When false the product will be excluded from listings, + but can still be provisioned directly if it's label is known. + Any pages that display information about the product when not listed, + should indicate to webcrawlers that the content should not be indexed. + marketing: + type: object + description: | + Object to hold various flags for marketing purposes only. These are values + that need to be stored, but should not affect decision making in code. If + we find ourselves in a position where we think they should, we should + consider refactoring our listing definition. + properties: + beta: + type: boolean + default: false + description: | + Indicates whether or not the product is in `Beta` and should be + advertised as such. This does not have any impact on who can access the + product, it is just used to inform consumers through our clients. + new: + type: boolean + default: false + description: | + Indicates whether or not the product is in `New` and should be + advertised as such. This does not have any impact on who can access the + product, it is just used to inform consumers through our clients. + featured: + type: boolean + default: false + description: | + Indicates whether or not the product is in `New` and should be + advertised as such. This does not have any impact on who can access the + product, it is just used to inform consumers through our clients. + additionalProperties: false + default: {} + x-go-type: + type: ProductListingMarketing + import: + package: 'github.com/manifoldco/catalog/client/primitives/shims' + alias: cShims + additionalProperties: false + default: {} + x-go-type: + type: ProductListing + import: + package: 'github.com/manifoldco/catalog/client/primitives/shims' + alias: cShims + + ProductProvisioning: + type: string + enum: + - 'provider-only' + - 'pre-order' + - 'public' + description: | + Provider Only, implies that the product should only be provisionable by the + provider; so members of the provider team, no one else should be allowed. + Pre-Order, should not be used yet. But in the future it should allow people to + pre-provision a resource for when it does go live. + Public, means the resource is live and everyone should be able to provision it. + + ProductIntegrationFeatures: + type: object + properties: + access_code: + description: | + Indicates whether or not this product supports resource transitions to + manifold by access_code. + type: boolean + x-nullable: false + sso: + description: | + Represents whether or not this product supports Single + Sign On + type: boolean + x-nullable: false + plan_change: + description: | + Represents whether or not this product supports changing + the plan of a resource. + type: boolean + x-nullable: false + region: + description: | + Describes how the region for a resource is specified, if + unspecified, then regions have no impact on this + resource. + type: string + enum: + - user-specified + - unspecified + credential: + description: | + Describes the credential type that is supported by this product. + + * `none`: The product does not support providing any credentials + * `single`: Only one credential is supported at the same time. + * `multiple`: Multiple credentials are supported at the same time. + * `unknown`: The credential type is unknown. + type: string + default: 'multiple' + enum: + - 'none' + - 'single' + - 'multiple' + - 'unknown' + x-nullable: false + additionalProperties: false + default: {} + x-go-type: + type: ProductIntegrationFeatures + import: + package: 'github.com/manifoldco/catalog/client/primitives/shims' + alias: cShims + + ProductBody: + type: object + properties: + provider_id: { $ref: '#/definitions/ID' } + label: + $ref: '#/definitions/Label' + description: | + Product labels are globally unique and contain the provider name. + name: { $ref: '#/definitions/Name' } + state: + $ref: '#/definitions/ProductState' + listing: { $ref: '#/definitions/ProductListing' } + logo_url: { $ref: '#/definitions/LogoURL' } + tagline: + description: | + 140 character sentence positioning the product. + type: string + maxLength: 140 + x-nullable: false + value_props: + description: A list of value propositions of the product. + type: array + items: { $ref: '#/definitions/ValueProp' } + maxItems: 8 + x-nullable: false + setup_steps: + description: A list of getting started steps for the product + type: array + items: + description: A markdown string describing a step in how to setup a product + type: string + minLength: 3 + maxLength: 200 + maxItems: 8 + x-nullable: true + images: + type: array + items: { $ref: '#/definitions/ProductImageURL' } + maxItems: 8 + x-nullable: false + support_email: + type: string + format: email + x-nullable: false + documentation_url: + type: string + format: url + terms: + description: | + URL to this Product's Terms of Service. If provided is true, then + a url must be set. Otherwise, provided is false. + type: object + properties: + url: { type: string, format: url, x-nullable: true } + provided: { type: boolean, x-nullable: false } + additionalProperties: false + required: + - provided + feature_types: + type: array + items: { $ref: '#/definitions/FeatureType' } + billing: + type: object + properties: + type: + type: string + enum: + - 'monthly-prorated' + - 'monthly-anniversary' + - 'annual-anniversary' + currency: { type: string, enum: ['usd'] } + additionalProperties: false + required: + - type + - currency + integration: + type: object + properties: + provisioning: + $ref: '#/definitions/ProductProvisioning' + base_url: { type: string, format: url } + sso_url: { type: string, format: url, x-nullable: true } + version: { type: string, enum: ['v1'] } + features: { $ref: '#/definitions/ProductIntegrationFeatures' } + additionalProperties: false + required: + - provisioning + - base_url + - version + - features + tags: { $ref: '#/definitions/ProductTags' } + additionalProperties: false + required: + - provider_id + - label + - name + - state + - listing + - logo_url + - tagline + - value_props + - images + - support_email + - terms + - documentation_url + - feature_types + - billing + - integration + + Product: + type: object + properties: + id: { $ref: '#/definitions/ID' } + version: { type: integer, enum: [1] } + type: { type: string, enum: ['product'] } + body: { $ref: '#/definitions/ProductBody' } + additionalProperties: false + required: + - id + - version + - type + - body + + CreateProduct: + type: object + properties: + body: { $ref: '#/definitions/ProductBody' } + additionalProperties: false + required: + - body + + PlanResizeList: + type: array + description: | + Array of Plan IDs that this Plan can be resized to, if null all will be assumed + items: { $ref: '#/definitions/ID' } + x-nullable: true + + PlanBody: + type: object + properties: + provider_id: { $ref: '#/definitions/ID' } + product_id: { $ref: '#/definitions/ID' } + name: { $ref: '#/definitions/Name' } + label: { $ref: '#/definitions/Label' } + state: { $ref: '#/definitions/PlanState' } + resizable_to: { $ref: '#/definitions/PlanResizeList' } + regions: + type: array + description: 'Array of Region IDs' + items: { $ref: '#/definitions/ID' } + features: + type: array + description: 'Array of Feature Values' + items: { $ref: '#/definitions/FeatureValue' } + maxItems: 20 + trial_days: + type: integer + minimum: 0 + description: | + The number of days a user gets as a free trial when subscribing to + this plan. Trials are valid only once per product; changing plans + or adding an additional subscription will not start a new trial. + cost: + type: integer + minimum: 0 + description: | + Dollar value in cents. + additionalProperties: false + additionalProperties: false + required: + - provider_id + - product_id + - name + - label + - state + - regions + - features + - cost + + PlanState: + type: string + enum: + - 'hidden' + - 'available' + - 'grandfathered' + - 'unlisted' + + ExpandedPlanBody: + type: object + allOf: + - { $ref: '#/definitions/PlanBody' } + - type: object + properties: + expanded_features: + type: array + description: > + An array of feature definitions for the plan, as defined on the Product. + items: { $ref: '#/definitions/ExpandedFeature' } + free: + type: boolean + description: > + A boolean flag that indicates if a plan is free or not based on it's cost and + features. + defaultCost: + type: integer + description: | + Plan cost using its default features plus base cost. + customizable: + type: boolean + description: > + A boolean flag that indicates if a plan has customizable features. + additionalProperties: false + required: + - expanded_features + - free + + ExpandedFeature: + type: object + allOf: + - { $ref: '#/definitions/FeatureType' } + - type: object + properties: + value_string: + type: string + description: > + The string value set for the feature on the plan, this should only be used if the + value property is null. + value: { $ref: '#/definitions/FeatureValueDetails' } + additionalProperties: false + required: + - value_string + - value + + Plan: + type: object + properties: + id: { $ref: '#/definitions/ID' } + version: { type: integer, enum: [1] } + type: { type: string, enum: ['plan'] } + body: { $ref: '#/definitions/PlanBody' } + additionalProperties: false + required: + - id + - version + - type + - body + + ExpandedPlan: + type: object + properties: + id: { $ref: '#/definitions/ID' } + version: { type: integer, enum: [1] } + type: { type: string, enum: ['plan'] } + body: { $ref: '#/definitions/ExpandedPlanBody' } + additionalProperties: false + required: + - id + - version + - type + - body + + CreatePlan: + type: object + properties: + body: { $ref: '#/definitions/PlanBody' } + additionalProperties: false + required: + - body + + Error: + type: object + description: 'Unexpected error' + properties: + type: + type: string + description: The error type + message: + type: array + description: Explanation of the errors + items: { type: string } + additionalProperties: false + required: + - type + - message + x-go-type: + type: Error + import: + package: 'github.com/manifoldco/go-manifold' + alias: manifold + + PriceFormula: + type: string + x-nullable: false + description: | + Describes how a feature cost should be calculated. An empty + string defaults to the normal price calculation using the value cost. + Formula uses Reverse Polish notation for statements. It supports + addition, subtraction and multiplication operations. Operations must be + grouped with parenthesis. + Number literals can be used for formulas. Eg: "(- feature-a#cost 500)" + will remove 5 dollars from the cost of feature a. + Multiplication operation supports either a cost multiplied by a + factor or a number multiplied by a factor. + In a plan formula the following keywords are available: + - `plan#base_cost` is the base cost of a plan in cents + - `plan#partial_cost` is the base cost plus its feature costs calculated + so far. Feature formulas are calculated in the order they are defined, + so features can refer to another feature values or the partial_cost of + the plan. + - `this-feature-label#multiply_factor` is the multiply_factor of this + feature as a float number. + - `another-feature-label#cost` is the cost of a feature matching the label + in cents. + - `another-feature-label#number` is the numeric value of a number feature + In a feature formula, plan base cost and total cost cannot be used + + ExpandedProduct: + type: object + properties: + id: { $ref: '#/definitions/ID' } + version: { type: integer, enum: [1] } + type: { type: string, enum: ['product'] } + body: { $ref: '#/definitions/ProductBody' } + plans: + type: array + items: { $ref: '#/definitions/ExpandedPlan' } + provider: { $ref: '#/definitions/Provider' } + additionalProperties: false + required: + - id + - version + - type + - body + - provider From eb73a760a897ac39343801d0035f764ce787d9ca Mon Sep 17 00:00:00 2001 From: Dakshraj Sharma Date: Thu, 27 May 2021 21:50:51 +0530 Subject: [PATCH 2/2] rebased and updated expected schemas with main to fix breaking tests --- tests/bin/expected/manifold.ts | 38 +++++++++++++++++----------------- tests/bin/expected/petstore.ts | 18 ++++++++-------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/bin/expected/manifold.ts b/tests/bin/expected/manifold.ts index 821bda220..819bb9976 100644 --- a/tests/bin/expected/manifold.ts +++ b/tests/bin/expected/manifold.ts @@ -618,11 +618,11 @@ export interface definitions { base_url?: string; sso_url?: string; version?: "v1"; - features?: { + features: { access_code?: boolean; sso?: boolean; plan_change?: boolean; - credential?: "none" | "single" | "multiple" | "unknown"; + credential: "none" | "single" | "multiple" | "unknown"; }; }; /** An array of platform ids to restrict this product for. */ @@ -662,24 +662,24 @@ export interface definitions { name: definitions["Name"]; type: "boolean" | "string" | "number"; /** This sets whether or not the feature can be customized by a consumer. */ - customizable?: boolean; + customizable: boolean; /** * This sets whether or not the feature can be upgraded by the consumer after the * resource has provisioned. Upgrading means setting a higher value or selecting a * higher element in the list. */ - upgradable?: boolean; + upgradable: boolean; /** * This sets whether or not the feature can be downgraded by the consumer after the * resource has provisioned. Downgrading means setting a lower value or selecting a * lower element in the list. */ - downgradable?: boolean; + downgradable: boolean; /** * Sets if this feature’s value is trackable from the provider, * this only really affects numeric constraints. */ - measurable?: boolean; + measurable: boolean; values?: definitions["FeatureValuesList"]; }; /** @@ -699,7 +699,7 @@ export interface definitions { * is selected or is default for the plan. * Cost is deprecated in favor of the `price.cost` field. */ - cost?: number; + cost: number; /** * Price describes the cost of a feature. It should be preferred over * the `cost` property. @@ -710,13 +710,13 @@ export interface definitions { * when this value is selected or is default for the plan. * Number features should use the cost range instead. */ - cost?: number; + cost: number; /** * When a feature is used to multiply the cost of the plan or of * another feature, multiply factor is used for calculation. * A feature cannot have both a cost and a multiply factor. */ - multiply_factor?: number; + multiply_factor: number; /** Price describes how the feature cost should be calculated. */ formula?: definitions["PriceFormula"]; /** Description explains how a feature is calculated to the user. */ @@ -737,9 +737,9 @@ export interface definitions { * means this numeric details has no scale, and will not be or customizable. * Some plans may not have a measureable or customizable feature. */ - increment?: number; + increment: number; /** Minimum value that can be set by a user if customizable */ - min?: number; + min: number; /** Maximum value that can be set by a user if customizable */ max?: number; /** Applied to the end of the number for display, for example the ‘GB’ in ‘20 GB’. */ @@ -758,7 +758,7 @@ export interface definitions { * An integer in 10,000,000ths of cents, will be multiplied by the * numeric value set in the feature to determine the cost. */ - cost_multiple?: number; + cost_multiple: number; }; FeatureValue: { feature: definitions["Label"]; @@ -784,7 +784,7 @@ export interface definitions { * When true, everyone can see the product when requested. When false it will * not be visible to anyone except those on the provider team. */ - public?: boolean; + public: boolean; /** * When true, the product will be displayed in product listings alongside * other products. When false the product will be excluded from listings, @@ -792,32 +792,32 @@ export interface definitions { * Any pages that display information about the product when not listed, * should indicate to webcrawlers that the content should not be indexed. */ - listed?: boolean; + listed: boolean; /** * Object to hold various flags for marketing purposes only. These are values * that need to be stored, but should not affect decision making in code. If * we find ourselves in a position where we think they should, we should * consider refactoring our listing definition. */ - marketing?: { + marketing: { /** * Indicates whether or not the product is in `Beta` and should be * advertised as such. This does not have any impact on who can access the * product, it is just used to inform consumers through our clients. */ - beta?: boolean; + beta: boolean; /** * Indicates whether or not the product is in `New` and should be * advertised as such. This does not have any impact on who can access the * product, it is just used to inform consumers through our clients. */ - new?: boolean; + new: boolean; /** * Indicates whether or not the product is in `New` and should be * advertised as such. This does not have any impact on who can access the * product, it is just used to inform consumers through our clients. */ - featured?: boolean; + featured: boolean; }; }; /** @@ -858,7 +858,7 @@ export interface definitions { * * `multiple`: Multiple credentials are supported at the same time. * * `unknown`: The credential type is unknown. */ - credential?: "none" | "single" | "multiple" | "unknown"; + credential: "none" | "single" | "multiple" | "unknown"; }; ProductBody: { provider_id: definitions["ID"]; diff --git a/tests/bin/expected/petstore.ts b/tests/bin/expected/petstore.ts index 59460406a..b0c74be46 100644 --- a/tests/bin/expected/petstore.ts +++ b/tests/bin/expected/petstore.ts @@ -72,12 +72,12 @@ export interface components { shipDate?: string; /** Order Status */ status?: "placed" | "approved" | "delivered"; - complete?: boolean; - } & { [key: string]: any }; + complete: boolean; + }; Category: { id?: number; name?: string; - } & { [key: string]: any }; + }; User: { id?: number; username?: string; @@ -88,11 +88,11 @@ export interface components { phone?: string; /** User Status */ userStatus?: number; - } & { [key: string]: any }; + }; Tag: { id?: number; name?: string; - } & { [key: string]: any }; + }; Pet: { id?: number; category?: components["schemas"]["Category"]; @@ -101,12 +101,12 @@ export interface components { tags?: components["schemas"]["Tag"][]; /** pet status in the store */ status?: "available" | "pending" | "sold"; - } & { [key: string]: any }; + }; ApiResponse: { code?: number; type?: string; message?: string; - } & { [key: string]: any }; + }; }; } @@ -221,7 +221,7 @@ export interface operations { name?: string; /** Updated status of the pet */ status?: string; - } & { [key: string]: any }; + }; }; }; }; @@ -264,7 +264,7 @@ export interface operations { additionalMetadata?: string; /** file to upload */ file?: string; - } & { [key: string]: any }; + }; }; }; };