UNPKG

@accordproject/cicero-core

Version:

Cicero Core - Implementation of Accord Protocol Template Specification

390 lines (362 loc) 13 kB
/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; var Logger = require('@accordproject/ergo-compiler').Logger; var ErgoCompiler = require('@accordproject/ergo-compiler').Compiler; var ciceroVersion = require('../package.json').version; var semver = require('semver'); var getMimeType = require('./mimetype'); var templateTypes = { CONTRACT: 0, CLAUSE: 1 }; var IMAGE_SIZE = { width: 128, height: 128 }; /** * Defines the metadata for a Template, including the name, version, README markdown. * @class * @public */ class Metadata { /** * Create the Metadata. * <p> * <strong>Note: Only to be called by framework code. Applications should * retrieve instances from {@link Template}</strong> * </p> * @param {object} packageJson - the JS object for package.json (required) * @param {String} readme - the README.md for the template (may be null) * @param {object} samples - the sample markdown for the template in different locales, * @param {object} request - the JS object for the sample request * @param {Buffer} logo - the bytes data for the image * represented as an object whose keys are the locales and whose values are the sample markdown. * For example: * { * default: 'default sample markdown', * en: 'sample text in english', * fr: 'exemple de texte français' * } * Locale keys (with the exception of default) conform to the IETF Language Tag specification (BCP 47). * THe `default` key represents sample template text in a non-specified language, stored in a file called `sample.md`. */ constructor(packageJson, readme, samples, request, logo) { // name of the runtime that this template targets (if the template contains compiled code) this.runtime = null; // the version of Cicero that this template is compatible with this.ciceroVersion = null; // the logo this.logo = null; if (!packageJson || typeof packageJson !== 'object') { throw new Error('package.json is required and must be an object'); } if (!packageJson.accordproject) { // Catches the previous format for the package.json with `cicero` if (packageJson.cicero && packageJson.cicero.version) { var msg = "The template targets Cicero (".concat(packageJson.cicero.version, ") but the Cicero version is ").concat(ciceroVersion, "."); Logger.error(msg); throw new Error(msg); } throw new Error('Failed to find accordproject metadata in package.json'); } if (!packageJson.accordproject.cicero) { throw new Error('Failed to find accordproject cicero version in package.json'); } if (!semver.validRange(packageJson.accordproject.cicero)) { throw new Error('The cicero version must be a valid semantic version (semver) range.'); } if (!semver.valid(packageJson.version)) { throw new Error('The template version must be a valid semantic version (semver) number.'); } this.ciceroVersion = packageJson.accordproject.cicero; if (!this.satisfiesCiceroVersion(ciceroVersion)) { var _msg = "The template targets Cicero version ".concat(this.ciceroVersion, " but the current Cicero version is ").concat(ciceroVersion, "."); Logger.error(_msg); throw new Error(_msg); } // the runtime property is optional, and is only mandatory for templates that have been compiled if (packageJson.accordproject.runtime && packageJson.accordproject.runtime !== 'ergo') { ErgoCompiler.isValidTarget(packageJson.accordproject.runtime); } else { packageJson.accordproject.runtime = 'ergo'; } this.runtime = packageJson.accordproject.runtime; if (!samples || typeof samples !== 'object') { throw new Error('sample.md is required'); } if (request && typeof request !== 'object') { throw new Error('request.json must be an object'); } if (!packageJson.name || !this._validName(packageJson.name)) { throw new Error('template name can only contain lowercase alphanumerics, _ or -'); } this.packageJson = packageJson; if (readme && typeof readme !== 'string') { throw new Error('README must be a string'); } if (!packageJson.keywords) { packageJson.keywords = []; } if (packageJson.keywords && !Array.isArray(packageJson.keywords)) { throw new Error('keywords property in package.json must be an array.'); } if (packageJson.displayName && packageJson.displayName.length > 214) { throw new Error('The template displayName property is limited to a maximum of 214 characters.'); } if (logo && logo instanceof Buffer) { Metadata.checkImage(logo); } else if (logo && !(logo instanceof Buffer)) { throw new Error('logo must be a Buffer'); } this.readme = readme; this.logo = logo; this.samples = samples; this.request = request; this.type = templateTypes.CONTRACT; if (packageJson.accordproject && packageJson.accordproject.template) { if (packageJson.accordproject.template !== 'contract' && packageJson.accordproject.template !== 'clause') { throw new Error('A cicero template must be either a "contract" or a "clause".'); } if (packageJson.accordproject.template === 'clause') { this.type = templateTypes.CLAUSE; } } else { Logger.warn('No cicero template type specified. Assuming that this is a contract template'); } } /** * check to see if it is a valid name. for some reason regex is not working when this executes * inside the chaincode runtime, which is why regex hasn't been used. * * @param {string} name the template name to check * @returns {boolean} true if valid, false otherwise * * @private */ _validName(name) { var validChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_']; for (var i = 0; i < name.length; i++) { var strChar = name.charAt(i); if (validChars.indexOf(strChar) === -1) { return false; } } return true; } /** * Returns either a 0 (for a contract template), or 1 (for a clause template) * @returns {number} the template type */ getTemplateType() { return this.type; } /** * Returns the logo at the root of the template * @returns {Buffer} the bytes data of logo */ getLogo() { return this.logo; } /** * Returns the author for this template. * @return {*} the author information */ getAuthor() { if (this.packageJson.author) { return this.packageJson.author; } else { return null; } } /** * Returns the name of the runtime target for this template, or null if this template * has not been compiled for a specific runtime. * @returns {string} the name of the runtime */ getRuntime() { return this.runtime; } /** * Returns the version of Cicero that this template is compatible with. * i.e. which version of the runtime was this template built for? * The version string conforms to the semver definition * @returns {string} the semantic version */ getCiceroVersion() { return this.ciceroVersion; } /** * Only returns true if the current cicero version satisfies the target version of this template * @param {string} version the cicero version to check against * @returns {string} the semantic version */ satisfiesCiceroVersion(version) { return semver.satisfies(version, this.getCiceroVersion(), { includePrerelease: true }); } /** * Returns the samples for this template. * @return {object} the sample files for the template */ getSamples() { return this.samples; } /** * Returns the sample request for this template. * @return {object} the sample request for the template */ getRequest() { return this.request; } /** * Returns the sample for this template in the given locale. This may be null. * If no locale is specified returns the default sample if it has been specified. * * @param {string} locale the IETF language code for the language. * @return {string} the sample file for the template in the given locale or null */ getSample() { var locale = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; if (!locale && 'default' in this.samples) { return this.samples.default; } else if (locale && locale in this.samples) { return this.samples[locale]; } else { return null; } } /** * Returns the README.md for this template. This may be null if the template does not have a README.md * @return {String} the README.md file for the template or null */ getREADME() { return this.readme; } /** * Returns the package.json for this template. * @return {object} the Javascript object for package.json */ getPackageJson() { return this.packageJson; } /** * Returns the name for this template. * @return {string} the name of the template */ getName() { return this.packageJson.name; } /** * Returns the display name for this template. * @return {string} the display name of the template */ getDisplayName() { // Fallback for packages that don't have a displayName property. if (!this.packageJson.displayName) { // Convert `acceptance-of-delivery` or `acceptance_of_delivery` into `Acceptance Of Delivery` return String(this.packageJson.name).split(/_|-/).map(word => word.replace(/^./, str => str.toUpperCase())).join(' ').trim(); } return this.packageJson.displayName; } /** * Returns the keywords for this template. * @return {Array} the keywords of the template */ getKeywords() { if (this.packageJson.keywords.length < 1 || this.packageJson.keywords === undefined) { return []; } else { return this.packageJson.keywords; } } /** * Returns the description for this template. * @return {string} the description of the template */ getDescription() { return this.packageJson.description; } /** * Returns the version for this template. * @return {string} the description of the template */ getVersion() { return this.packageJson.version; } /** * Returns the identifier for this template, formed from name@version. * @return {string} the identifier of the template */ getIdentifier() { return this.packageJson.name + '@' + this.packageJson.version; } /** * Check the buffer is a png file with the right size * @param {Buffer} buffer the buffer object */ static checkImage(buffer) { var mimeType = getMimeType(buffer).mime; Metadata.checkImageDimensions(buffer, mimeType); } /** * Checks if dimensions for the image are correct. * @param {Buffer} buffer the buffer object * @param {string} mimeType the mime type of the object */ static checkImageDimensions(buffer, mimeType) { var height; var width; if (mimeType === 'image/png') { try { height = buffer.readUInt32BE(20); width = buffer.readUInt32BE(16); } catch (err) { throw new Error('not a valid png file'); } } else { throw new Error('dimension calculation not supported for this file'); } if (height === IMAGE_SIZE.height && width === IMAGE_SIZE.width) { return; } else { throw new Error("logo should be ".concat(IMAGE_SIZE.height, "x").concat(IMAGE_SIZE.width)); } } /** * Return new Metadata for a target runtime * @param {string} runtimeName - the target runtime name * @return {object} the new Metadata */ createTargetMetadata(runtimeName) { var packageJson = JSON.parse(JSON.stringify(this.packageJson)); packageJson.accordproject.runtime = runtimeName; return new Metadata(packageJson, this.readme, this.samples, this.request, this.logo); } /** * Return the whole metadata content, for hashing * @return {object} the content of the metadata object */ toJSON() { return { 'packageJson': this.getPackageJson(), 'readme': this.getREADME(), 'samples': this.getSamples(), 'request': this.getRequest(), 'logo': this.getLogo() ? this.getLogo().toString('base64') : null }; } } module.exports = Metadata;