Skip to content

Commit

Permalink
test(json-schema) : test coverage
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Selman <[email protected]>
  • Loading branch information
dselman committed Sep 23, 2020
1 parent 503fbb6 commit a95e1fd
Show file tree
Hide file tree
Showing 8 changed files with 575 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ describe('GitHubModelFileLoader', () => {

it('should load github URIs', () => {
// Match against an exact URL value
moxios.stubRequest('https://raw.githubusercontent.com/accordproject/models/master/usa/business.cto', {
moxios.stubRequest('https://raw.githubusercontent.com/accordproject/models/master/src/usa/business.cto', {
status: 200,
responseText: model
});

const ml = new GitHubModelFileLoader(modelManager);
return ml.load( 'github://accordproject/models/master/usa/business.cto', {foo: 'bar' })
return ml.load( 'github://accordproject/models/master/src/usa/business.cto', {foo: 'bar' })
.then((mf) => {
mf.getDefinitions().should.be.deep.equal(model);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,9 @@ describe('HTTPModeFilelLoader', () => {
let modelManager;
let sandbox;

let model = `namespace org.accordproject.usa.business
/**
* Types of businesses in the USA
* Taken from: https://en.wikipedia.org/wiki/List_of_business_entities#United_States
*/
enum BusinessEntity {
o GENERAL_PARTNERSHIP
o LP
o LLP
o LLLP
o LLC
o PLLC
o CORP
o PC
o DBA
let model = `namespace test
enum Test {
o ONE
}`;

beforeEach(() => {
Expand Down Expand Up @@ -81,13 +68,15 @@ describe('HTTPModeFilelLoader', () => {
it('should load https URIs', () => {

// Match against an exact URL value
moxios.stubRequest('https://raw.githubusercontent.com/accordproject/models/master/usa/business.cto', {
const url = 'https://raw.githubusercontent.com/accordproject/models/master/src/usa/business.cto';

moxios.stubRequest(url, {
status: 200,
responseText: model
});

const ml = new HTTPModelFileLoader(modelManager);
return ml.load('https://raw.githubusercontent.com/accordproject/models/master/usa/business.cto')
return ml.load(url)
.then((mf) => {
mf.getDefinitions().should.be.deep.equal(model);
});
Expand Down
12 changes: 6 additions & 6 deletions packages/concerto-core/test/introspect/numbervalidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@ describe('NumberValidator', () => {
describe('#constructor', () => {
it('should accept valid constructor parms VALID_UPPER_AND_LOWER_BOUND_AST', () => {
let validator = new NumberValidator(mockField, VALID_UPPER_AND_LOWER_BOUND_AST);
validator.lowerBound.should.equal(0);
validator.upperBound.should.equal(100);
validator.getLowerBound().should.equal(0);
validator.getUpperBound().should.equal(100);
});

it('should accept valid constructor parms NO_LOWER_BOUND_AST', () => {
let validator = new NumberValidator(mockField, NO_LOWER_BOUND_AST);
should.equal(validator.lowerBound, null);
validator.upperBound.should.equal(100);
should.equal(validator.getLowerBound(), null);
validator.getUpperBound().should.equal(100);
});

it('should accept valid constructor parms NO_UPPER_BOUND_AST', () => {
let validator = new NumberValidator(mockField, NO_UPPER_BOUND_AST);
validator.lowerBound.should.equal(0);
should.equal(validator.upperBound, null);
validator.getLowerBound().should.equal(0);
should.equal(validator.getUpperBound(), null);
});

it('should throw an error for constructor parms NO_PARMS_IN_AST', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/concerto-core/test/introspect/stringvalidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('StringValidator', () => {

it('should ignore a null string', () => {
let v = new StringValidator(mockField, '/^[A-z][A-z][0-9]{7}/' );
v.getRegex().toString().should.equal('/^[A-z][A-z][0-9]{7}/');
v.validate('id', null);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,22 @@ const RelationshipDeclaration = require('@accordproject/concerto-core').Relation
const TransactionDeclaration = require('@accordproject/concerto-core').TransactionDeclaration;
const debug = require('debug')('concerto-core:jsonschemavisitor');
const util = require('util');
const RecursionDetectionVisitor = require('./recursionvisitor');

/**
* Convert the contents of a {@link ModelManager} instance to a set of JSON
* Schema v4 files - one per concrete asset and transaction type.
* Set a fileWriter property (instance of {@link FileWriter}) on the parameters
* object to control where the generated code is written to disk.
* Convert the contents of a {@link ModelManager} to a JSON Schema, returning
* the schema for all types under the 'definitions' key. If the 'rootType'
* parameter option is set to a fully-qualified type name, then the properties
* of the type are also added to the root of the schema object.
*
* If the fileWriter parameter is set then the JSONSchema will be written to disk.
*
* Note that by default $ref is used to references types, unless
* the `inlineTypes` parameter is set, in which case types are expanded inline,
* UNLESS they contain recursive references, in which case $ref is used.
*
* The meta schema used is http://json-schema.org/draft-07/schema#
*
* @private
* @class
* @memberof module:concerto-tools
Expand All @@ -53,6 +63,21 @@ class JSONSchemaVisitor {
: null;
}

/**
* Returns true if the class declaration contains recursive references.
*
* Basic example:
* concept Person {
* o Person[] children
* }
*
* @param {object} classDeclaration the class being visited
*/
isModelRecursive(classDeclaration) {
const visitor = new RecursionDetectionVisitor();
return classDeclaration.accept( visitor, {stack : []} );
}

/**
* Visitor design pattern
* @param {Object} thing - the object being visited
Expand Down Expand Up @@ -96,9 +121,6 @@ class JSONSchemaVisitor {
visitModelManager(modelManager, parameters) {
debug('entering visitModelManager');

// Save the model manager so that we have access to it later.
parameters.modelManager = modelManager;

// Visit all of the files in the model manager.
let result = {
$schema : 'http://json-schema.org/draft-07/schema#' // default for https://github.com/ajv-validator/ajv
Expand All @@ -113,6 +135,13 @@ class JSONSchemaVisitor {
result = { ... result, ... schema.schema };
}

if(parameters.fileWriter) {
const fileName = parameters.rootType ? `${parameters.rootType}.json` : 'schema.json';
parameters.fileWriter.openFile(fileName);
parameters.fileWriter.writeLine(0, JSON.stringify(result, null, 2));
parameters.fileWriter.closeFile();
}

return result;
}

Expand All @@ -126,9 +155,6 @@ class JSONSchemaVisitor {
visitModelFile(modelFile, parameters) {
debug('entering visitModelFile', modelFile.getNamespace());

// Save the model file so that we have access to it later.
parameters.modelFile = modelFile;

// Visit all of the asset and transaction declarations, but ignore the abstract ones.
let result = {
definitions : {}
Expand Down Expand Up @@ -203,6 +229,8 @@ class JSONSchemaVisitor {
visitClassDeclarationCommon(classDeclaration, parameters) {
debug('entering visitClassDeclarationCommon', classDeclaration.getName());

parameters.inlineTypes = parameters.inlineTypes ? !this.isModelRecursive(classDeclaration) : false;

const result = {
$id: classDeclaration.getFullyQualifiedName(),
schema: {
Expand All @@ -217,7 +245,7 @@ class JSONSchemaVisitor {
result.schema.properties.$class = {
type: 'string',
default: classDeclaration.getFullyQualifiedName(),
pattern: classDeclaration.getFullyQualifiedName().split('.').join('\\.'),
pattern: `^${classDeclaration.getFullyQualifiedName().split('.').join('\\.')}$`,
description: 'The class identifier for this type'
};

Expand Down Expand Up @@ -319,10 +347,14 @@ class JSONSchemaVisitor {

// Not primitive, so must be a class or enumeration!
} else {

// Look up the type of the property.
let type = parameters.modelFile.getModelManager().getType(field.getFullyQualifiedTypeName());
jsonSchema = { $ref: `#/definitions/${type.getFullyQualifiedName()}` };
let type = field.getParent().getModelFile().getModelManager().getType(field.getFullyQualifiedTypeName());
if(!parameters.inlineTypes) {
jsonSchema = { $ref: `#/definitions/${type.getFullyQualifiedName()}` };
} else {
// inline the schema
jsonSchema = this.visit( type, parameters ).schema;
}
}

// Is the type an array?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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';

const ClassDeclaration = require('@accordproject/concerto-core').ClassDeclaration;
const EnumDeclaration = require('@accordproject/concerto-core').EnumDeclaration;
const Field = require('@accordproject/concerto-core').Field;
const RelationshipDeclaration = require('@accordproject/concerto-core').RelationshipDeclaration;
const debug = require('debug')('concerto-core:recursiondetectionvisitor');
const util = require('util');

/**
* Detects whether ClassDeclaration contains recursive references.
* Basic example:
* concept Person {
* o Person[] children
* }
*
* parameters.stack should be initialized to []
* @private
* @class
* @memberof module:concerto-tools
*/
class RecursionDetectionVisitor {

/**
* Visitor design pattern
* @param {Object} thing - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visit(thing, parameters) {
// the order of these matters!
if (thing instanceof EnumDeclaration) {
return this.visitEnumDeclaration(thing, parameters);
}
else if (thing instanceof ClassDeclaration) {
return this.visitClassDeclaration(thing, parameters);
} else if (thing instanceof Field) {
return this.visitField(thing, parameters);
} else if (thing instanceof RelationshipDeclaration) {
return this.visitRelationshipDeclaration(thing, parameters);
} else {
throw new Error('Unrecognised type: ' + typeof thing + ', value: ' + util.inspect(thing, { showHidden: true, depth: null }));
}
}

/**
* Visitor design pattern
* @param {ClassDeclaration} classDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitClassDeclaration(classDeclaration, parameters) {
debug('entering visitClassDeclaration', classDeclaration.getName());

parameters.stack.push(classDeclaration.getFullyQualifiedName());

// Walk over all of the properties of this class and its super classes.
const properties = classDeclaration.getProperties();
for(let n=0; n < properties.length; n++) {
const property = properties[n];
if( property.accept(this, parameters) ) {
return true;
}
}

parameters.stack.pop();
return false;
}

/**
* Visitor design pattern
* @param {Field} field - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitField(field, parameters) {
debug('entering visitField', field.getName());
if(field.isPrimitive()) {
return false;
}

let type = field.getParent().getModelFile().
getModelManager().getType(field.getFullyQualifiedTypeName());

debug('stack', parameters.stack );
if( parameters.stack.includes(type.getFullyQualifiedName()) ) {
return true;
}
else {
return this.visit(type, parameters);
}
}

/**
* Visitor design pattern
* @param {EnumDeclaration} enumDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitEnumDeclaration(enumDeclaration, parameters) {
debug('entering visitEnumDeclaration', enumDeclaration.getName());
return false;
}

/**
* Visitor design pattern
* @param {RelationshipDeclaration} relationshipDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitRelationshipDeclaration(relationshipDeclaration, parameters) {
debug('entering visitRelationship', relationshipDeclaration.getName());
return false;
}
}

module.exports = RecursionDetectionVisitor;
Loading

0 comments on commit a95e1fd

Please sign in to comment.