UNPKG

apikana

Version:

Integrated tools for REST API design - アピ

813 lines (722 loc) 30.3 kB
;"use strict"; const JavaParser = require("java-parser"); const PathV3Generator = require("../../src/path-v3-generator/path-v3-generator"); const StreamUtils = require("../../src/util/stream-utils"); function noop(){} describe( "PathV3Generator" , ()=>{ it( "Failfast when get invoked with an illegal javaPackage" , ( done )=>{ const illegalPackages = [ "space.not allowed", "hyphen-not.allowed", "+", "-", true, ""/*empty string*/, "42", "com.9number", ]; for( var i=0 ; i<illegalPackages.length ; ++i ){ const javaPackage = illegalPackages[ i ]; try{ PathV3Generator.createPathV3Generator({ openApi:{ info: { title:"asdf" } }, javaPackage: javaPackage }); fail( "Expected to throw when using javaPackage=\""+javaPackage+"\"" ); }catch( err ){ expect( err.message ).toMatch( /javaPackage/i ); } } done(); }); it( "Generates path classes based on title from openapi model" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "My foo Api", }, paths: {} }, javaPackage: "com.example" }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const lines = result.split( '\n' ); for( var i=0 ; i<lines.length ; ++i ){ const line = lines[i]; const m = /^public static class (.*) {$/.exec( line ); if( m ){ const className = m[1]; expect( className ).toEqual( "MyFooApi" ); break; } } done(); } }); it( "Places generated class into specified package" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "asdf", }, paths: {}, }, javaPackage: "com.example.lib.my.api.v1.path", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const lines = result.split( '\n' ); var firstNonEmptyLine = ""; for( var i=0 ; firstNonEmptyLine.length < 1 && i<lines.length ; ++i ){ firstNonEmptyLine = lines[i]; } expect( firstNonEmptyLine ).toEqual( "package com.example.lib.my.api.v1.path;" ); done(); } }); it( "Provides first segment after specified basePath" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "winnie poo", }, paths: { "/my/api/v1/foo/blubb": null, "/my/api/v1/bar/blubb": null, } }, pathPrefix: "/my/api/v1/", javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compilationUnit = JavaParser.parse( result , {} ); const clazz = compilationUnit.types[0]; const foo = clazz.bodyDeclarations.filter( e => e.name && e.name.identifier==="foo" )[0]; expect( foo ).toBeTruthy(); const bar = clazz.bodyDeclarations.filter( e => e.name && e.name.identifier==="bar" )[0]; expect( bar ).toBeTruthy(); done(); } }); it( "Provides MyApi.one.two.three when using path '/my/api/v1/one/two/three'" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "qwer qwer API", }, paths: { "/my/api/v1/one/two/three": null, } }, javaPackage: "com.example", pathPrefix: "/my/api/v1/", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compilationUnit = JavaParser.parse( result , {} ); const clazz = compilationUnit.types[0]; const one = clazz.bodyDeclarations.filter( e => e.name && e.name.identifier==="one" )[0]; const two = one.bodyDeclarations.filter( e => e.name && e.name.identifier==="two" )[0]; const three = two.bodyDeclarations.filter( e => e.name && e.name.identifier==="three" )[0]; const res = three.bodyDeclarations.filter( e => e.fragments && e.fragments[0].name.identifier==="RESOURCE" )[0]; expect( res.fragments[0].initializer.escapedValue ).toEqual( '"/my/api/v1/one/two/three"' ); done(); } }); it( "Provides RESOURCE identifier with leading, but without trailing slash" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { "info": { "title": "My bar API" }, "paths": { "/store-inventory": null, "/2nd/try": null, "/what.about.dots": null, "/are/you/sure?": null, "/are/you/a-genious": null, "/a space": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compileUnit = JavaParser.parse( result , {}); const values = collectAllValuesOfResourceConstants( compileUnit ); expect( values.length ).toBe( 24 ); for( let i=0 ; i<values.length ; ++i ){ const value = values[i]; expect( value ).toMatch( /^"\// ); // MUST HAVE leading slash. expect( value ).toMatch( /[^\/]"$/ ); // MUST NOT HAVE trailing slash. } done(); } function collectAllValuesOfResourceConstants( node ){ const resourceValues = []; createJavaParserNodeIterator( node ).forEach(function( node ){ if( node.node==="FieldDeclaration" ){ if( node.fragments.length > 1 ) throw Error( "Unexpected state" ); const name = node.fragments[0].name.identifier; if( name === "RESOURCE" ){ const value = node.fragments[0].initializer.escapedValue; resourceValues.push( value ); } } }); return resourceValues; } }); it( "Provides COLLECTION identifier with leading and trailing slash" , function( done ){ // Hint: This test doesn't check if there's a leading slash. const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "asdfsadf", }, paths: { "/customer": null, "/customer/{id}": null, "/customer/{id}/name": null, "/customer/{id}/contact": null, "/customer/{id}/contact/postal": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compileUnit = JavaParser.parse( result , {}); const values = collectAllValuesOfCollectionConstants( compileUnit ); expect( values.length ).toBe( 18 ); for( let i=0 ; i<values.length ; ++i ){ const value = values[i]; expect( value ).toEqual( 'RESOURCE + "/"' ); } done(); } function collectAllValuesOfCollectionConstants( node ){ const resourceValues = []; createJavaParserNodeIterator( node ).forEach(function( node ){ if( node.node==="FieldDeclaration" ){ if( node.fragments.length > 1 ) throw Error( "Unexpected state" ); const name = node.fragments[0].name.identifier; if( name === "COLLECTION" ){ const initializer = node.fragments[0].initializer; const value = initializer.leftOperand.identifier + " "+ initializer.operator + " "+ initializer.rightOperand.escapedValue ; resourceValues.push( value ); } } }); return resourceValues; } }); it( "Provides BASED identifier where we can continue with follow-up segments" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "foo bar api", }, paths: { "/foo/bar/v1/pet/{id}/foo/bar": null, }, }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compileUnit = JavaParser.parse( result , {}); const clazz = compileUnit.types[0]; const fooSegment = clazz.bodyDeclarations.filter( e => e.name.identifier==="foo" )[0]; expect( fooSegment ).toBeTruthy(); const based = fooSegment.bodyDeclarations.filter( e => e.name && e.name.identifier==="BASED" )[0]; expect( based ).toBeTruthy(); // Check if BASED class has expected content. const barSegment = based.bodyDeclarations.filter( e => e.name.identifier==="bar" )[0]; expect( barSegment ).toBeTruthy(); done(); } }); it( "The BASED identifier is only available once in a chain" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "abc def ghi", }, paths: { "/one/two/three/four/five/six": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compilationUnit = JavaParser.parse( result , {} ); const clazz = compilationUnit.types[0]; const one = clazz.bodyDeclarations.filter( e => e.name.identifier==="one" )[0]; const two = one.bodyDeclarations.filter( e => e.name && e.name.identifier==="two" )[0]; const based = two.bodyDeclarations.filter( e => e.name && e.name.identifier==="BASED" )[0]; const three = based.bodyDeclarations.filter( e => e.name.identifier==="three" )[0]; const threeBased = three.bodyDeclarations.filter( e => e.name && e.name.identifier==="BASED" )[0]; expect( threeBased ).toBeFalsy(); done(); } }); it( "Puts only segments after BASED identifier into the constant" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "my root class", }, paths: { "/one/two/three": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compileUnit = JavaParser.parse( result , {} ); // Extract MyRootClass const clazz = compileUnit.types[0]; expect( clazz.name.identifier ).toEqual( "MyRootClass" ); // Extract MyRootClass.one const oneSegment = clazz.bodyDeclarations.filter( e => e.name && e.name.identifier==="one" )[0]; expect( oneSegment ).toBeTruthy(); // Extract MyRootClass.one.BASED const based = oneSegment.bodyDeclarations.filter( e => e.name && e.name.identifier==="BASED" )[0]; expect( based ).toBeTruthy(); // Extract MyRootClass.one.BASED.two const basedTwoSegment = based.bodyDeclarations.filter( e => e.name && e.name.identifier==="two" )[0]; expect( basedTwoSegment ).toBeTruthy(); // Extract MyRootClass.one.BASED.two.RESOURCE const resourceConstant = basedTwoSegment.bodyDeclarations.filter( e => e.fragments && e.fragments[0].name.identifier==="RESOURCE" )[0]; expect( resourceConstant ).toBeTruthy(); const value = resourceConstant.fragments[0].initializer.escapedValue; // Absolute path would be "/one/two". But because we used ".one.BASED.two" we // now expect to only get most right segment. expect( value ).toEqual( '"/two"' ); done(); } }); it( "Adds a dollar sign to segments which are a variable" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "foo bar api", }, paths: { "/pet/{petId}/info": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compilationUnit = JavaParser.parse( result , {} ); const clazz = compilationUnit.types[0]; const pet = clazz.bodyDeclarations.filter( e => e.name.identifier==="pet" )[0]; const petId = pet.bodyDeclarations.filter( e => e.name && ~e.name.identifier.indexOf("petId") )[0]; // Check trailing dollar exists. expect( petId.name.identifier ).toEqual( "petId$" ); const info = petId.bodyDeclarations.filter( e => e.name && ~e.name.identifier.indexOf("info") )[0]; // Also check next segment does NOT have that trailing dollar. expect( info.name.identifier ).toEqual( "info" ); done(); } }); it( "Replaces chars not allowed in java identifiers by an underscore char" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "asdf asdf foo", }, paths: { "/store-inventory": null, "/what.about.dots": null, "/are/you/sure?": null, "/are/you/a-genious": null, "/a space": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compilationUnit = JavaParser.parse( result , {} ); const resources = []; createJavaParserNodeIterator( compilationUnit ).forEach(function( node ){ if( node.node==="TypeDeclaration" && node.name.identifier !== "AsdfAsdfFoo" && node.name.identifier !== "BASED" ){ resources.push( node ); } }); resources.forEach(function( elem ){ const whitelist = [ "a_genious", "a_space", "are", "store_inventory", "sure_", "what_about_dots", "you", ]; expect( whitelist ).toContain( elem.name.identifier ); }); done(); } }); it( "Will fail when substitution of illegal chars would produce a name conflict" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "my grand pa", }, paths: { // Both of them will result in same path because numbers both get replaced by // underscore. "/my/foo/v1/2pack/two": null, "/my/foo/v1/6pack/six": null, } }, javaPackage: "com.example", }); victim.readable() .on( "data" , noop ) .on( "finish" , function(){ expect( "This method" ).toBe( "never called" ); done(); }) .on( "error" , function onError( err ) { const msg = err.message; expect( msg ).toContain( "2pack" ); expect( msg ).toContain( "6pack" ); expect( msg ).toContain( "my/foo/v1/_pack" ); done(); }) ; }); it( "Prepends an additional underscore char to generated identifier when they're a reserved word in java" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "EoarheAoiuno" }, paths: { // Some reserved words found at "https://www.thoughtco.com/reserved-words-in-java-2034200". "/abstract/assert/boolean/break/byte/case": null, "/catch/char/class/const/continue/default": null, "/double/do/else/enum/extends/false": null, "/final/finally/float/for/goto/if": null, "/implements/import/instanceof/int/interface/long": null, "/native/new/null/package/private/protected": null, "/public/return/short/static/strictfp/super": null, "/switch/synchronized/this/throw/throws/transient": null, "/true/try/void/volatile/while": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compilationUnit = JavaParser.parse( result , {}); const resources = []; createJavaParserNodeIterator( compilationUnit ).forEach(function( node ){ if( node.node==="TypeDeclaration" && node.name.identifier !== "EoarheAoiuno" && node.name.identifier !== "BASED" ){ resources.push( node ); } }); const whitelist = [ "_abstract", "_assert", "_boolean", "_break", "_byte", "_case", "_catch", "_char", "_class", "_const", "_continue", "_default", "_double", "_do", "_else", "_enum", "_extends", "_false", "_final", "_finally", "_float", "_for", "_goto", "_if", "_implements", "_import", "_instanceof", "_int", "_interface", "_long", "_native", "_new", "_null", "_package", "_private", "_protected", "_public", "_return", "_short", "_static", "_strictfp", "_super", "_switch", "_synchronized", "_this", "_throw", "_throws", "_transient", "_true", "_try", "_void", "_volatile", "_while", ]; expect( resources.length ).toEqual( 236 ); resources.forEach(function( elem ){ expect( whitelist ).toContain( elem.name.identifier ); }); done(); } }); // See: "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathsObject". it( "Failfast when missing slash at begin of path" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "tick trick track", }, paths: { "this/path/is/missing/a/leading/slash": null, }, }, javaPackage: "com.example", }); victim.readable() .on( "data" , noop ) .on( "error" , function( err ){ const msg = err.message; expect( msg ).toMatch( /slash/i ); expect( msg ).toContain( "this/path/is/missing/a/leading/slash" ); setTimeout( done ); }) .on( "end" , function(){ expect( "this function" ).toBe( "never called" ); setTimeout( done ); }) ; }); it( "Failfast when paths contain empty segments" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "tick trick track", }, paths: { "/this/path/contains//two/separators/inside": null, } }, javaPackage: "com.example", }); victim.readable() .on( "data" , noop ) .on( "end" , function(){ expect( "this function" ).toBe( "never called" ); setTimeout( done ); }) .on( "error" , function( err ){ const msg = err.message; expect( msg ).toMatch( /slash/i ); expect( msg ).toMatch( /double/i ); expect( msg ).toContain( "/this/path/contains//two/separators/inside" ); setTimeout( done ); }) ; }); it( "Failfast when api has slashes at end of path" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "tick trick track", }, paths: { "/this/path/en-ds/with/an/em-pty/seg-ment/": null, }, }, javaPackage: "com.example", }); victim.readable() .on( "data" , noop ) .on( "end" , function(){ expect( "this funciton" ).toBe( "never called" ); setTimeout( done ); }) .on( "error" , function( err ){ const msg = err.message; expect( msg ).toMatch( /segment/i ); expect( msg ).toMatch( /empty/i ); expect( msg ).toMatch( /end/i ); expect( msg ).toContain( "/this/path/en-ds/with/an/em-pty/seg-ment/" ); setTimeout( done ); }) ; }); it( "Will append an undersocre everytime same segment occurs again in same path" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "foo bar" }, paths: { "/foo/foo/bar/foo/bar/bar/foo/foo": null, } }, javaPackage: "com.example", }); victim.readable() .pipe( StreamUtils.createStringWritable() ) .then( assertResult ) ; function assertResult( result ){ const compilationUnit = JavaParser.parse( result , {}); const expectedIdentifierNames = [ "foo", "foo_", "bar", "foo__", "bar_", "bar__", "foo___", "foo____", ]; var i = 0; var isDone = false; const identifierNames = []; createJavaParserNodeIterator( compilationUnit ).forEach(function( node ){ if( isDone ){ // Ignore }else if( node.node==="TypeDeclaration" ){ if( node.name.identifier==="BASED" ){ expect( identifierNames.length ).toEqual( expectedIdentifierNames.length ); isDone = true; }else if( node.name.identifier !== "FooBar" && node.name.identifier !== "BASED" ){ const name = node.name.identifier; expect( name ).toEqual( expectedIdentifierNames[i] ); identifierNames.push( name ); i += 1; } } }); done(); } }); it( "Will fail-fast if 1st segment of a path doesn't match path-prefix" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "foo blubb API", }, paths: { "/foo/v1/one": null, "/foo/v1/two": null, "/bar/v1/a-bad-one": null, "/foo/v1/three": null, } }, javaPackage: "com.example", pathPrefix: "foo/v1/", }); const myFail = function( ctxt ){ if( ctxt.alreadyFailed ){ return; }else{ ctxt.alreadyFailed=true; } fail( "No data expected" ); done(); }.bind(0,{ alreadyFailed:false }); victim.readable() // Neither data nor end expected. .on( "data" , myFail ).on( "end" , myFail ) // We only expect an error. .on( "error" , function( err ){ const msg = err.message; // The path is mentioned. expect( msg ).toContain( "/bar/v1/a-bad-one" ); // Also the path-prefix is mentioned. expect( msg ).toContain( "foo/v1/" ); done(); }) ; }); it( "Will fail-fast if there's a segment somewhere in path not matching path-prefix" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "foo blubb API", }, paths: { "/foo/blubb/api/v1/one": null, "/foo/blubb/api/v2/a-bad-one": null, "/foo/blubb/api/v1/two": null, "/foo/blubb/api/v1/three": null, } }, javaPackage: "com.example", pathPrefix: "foo/blubb/api/v1/", }); const myFail = function( ctxt ){ if( ctxt.alreadyFailed ){ return; }else{ ctxt.alreadyFailed=true; } fail( "No data expected" ); done(); }.bind(0,{ alreadyFailed:false }); victim.readable() // Neither data nor end expected. .on( "data" , myFail ).on( "end" , myFail ) // We only expect an error. .on( "error" , function( err ){ const msg = err.message; // The path is mentioned. expect( msg ).toContain( "/foo/blubb/api/v2/a-bad-one" ); // Also the path-prefix is mentioned. expect( msg ).toContain( "foo/blubb/api/v1/" ); done(); }) ; }); it( "Will fail-fast if a single path mismatches path-prefix" , function( done ){ const victim = PathV3Generator.createPathV3Generator({ openApi: { info: { title: "foo blubb API", }, paths: { "/foo/v2/bad/version": null, } }, javaPackage: "com.example", pathPrefix: "/foo/v1/", }); const myFail = function( ctxt ){ if( ctxt.alreadyFailed ){ return; }else{ ctxt.alreadyFailed=true; } fail( "No data expected" ); done(); }.bind(0,{ alreadyFailed:false }); victim.readable() // Neither data nor end expected. .on( "data" , myFail ).on( "end" , myFail ) // We only expect an error. .on( "error" , function( err ){ const msg = err.message; // The path is mentioned. expect( msg ).toContain( "/foo/v2/bad/version" ); // Also the path-prefix is mentioned. expect( msg ).toContain( "/foo/v1/" ); done(); }) ; }); }); function createJavaParserNodeIterator( node ){ return { forEach: function forEach( cback ){ if( node.node === "CompilationUnit" || node.node === "TypeDeclaration" ){ cback( node ); (node.types||[]).forEach(function( elem ){ createJavaParserNodeIterator( elem ).forEach( cback ); }); (node.bodyDeclarations||[]).forEach(function( bodyDecl ) { createJavaParserNodeIterator( bodyDecl ).forEach( cback ); }); }else if( node.node === "MethodDeclaration" || node.node==="FieldDeclaration" ){ cback( node ); }else{ throw Error( "Not impl yet" ); } } }; }