Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions models/OpenAPIConstraintsGenerator.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
component name="OpenAPIConstraintsGenerator" {

property name="cache" inject="cachebox:template";

public struct function generateConstraintsFromOpenAPISchema(
string parametersPath = "",
string requestBodyPath = "",
boolean discoverPaths = true,
string callingFunctionName
){
var paths = _discoverPaths( argumentCollection = arguments );

var constraints = {};

if ( paths.parametersPath != "" ) {
var parametersJSON = variables.cache.getOrSet(
paths.parametersPath,
() => {
return deserializeJSON( fileRead( expandPath( paths.parametersPath ) ) );
},
1 // 1 day
);
structAppend( constraints, generateConstraintsFromParameters( parametersJSON[ paths.parametersKey ] ) )
}

if ( paths.requestBodyPath != "" ) {
var requestBodyJSON = variables.cache.getOrSet(
paths.requestBodyPath,
() => {
return deserializeJSON( fileRead( expandPath( paths.requestBodyPath ) ) );
},
1 // 1 day
);
var schema = requestBodyJSON[ "content" ][ "application/json" ][ "schema" ];
structAppend(
constraints,
generateConstraintsFromRequestBodyProperties( schema.properties, schema.required )
);
}

return constraints;
}

private struct function generateConstraintsFromParameters( required array parameters ){
return arguments.parameters.reduce( ( allConstraints, parameter ) => {
allConstraints[ parameter.name ] = generateConstraint(
schema = ( parameter.schema ?: {} ),
isRequired = ( parameter.required ?: false )
);
return allConstraints;
}, {} );
}

private struct function generateConstraintsFromRequestBodyProperties(
required struct properties,
required array requiredFields
){
return arguments.properties.map( ( fieldName, schema ) => {
return generateConstraint(
schema = schema,
isRequired = arrayContainsNoCase( requiredFields, fieldName ) > 0
);
} );
}

private struct function generateConstraint( required struct schema, required boolean isRequired ){
var constraints = {};
constraints[ "required" ] = arguments.isRequired;
addValidationType( constraints, schema );
if ( constraints[ "type" ] == "struct" && schema.keyExists( "properties" ) ) {
constraints[ "constraints" ] = generateConstraintsFromRequestBodyProperties(
schema.properties,
schema.required ?: []
);
}
if ( constraints[ "type" ] == "array" && schema.keyExists( "items" ) ) {
constraints[ "items" ] = generateConstraint( schema.items, arguments.isRequired );
}
if ( schema.keyExists( "enum" ) ) {
constraints[ "inList" ] = arrayToList( schema[ "enum" ] );
}
if ( schema.keyExists( "minimum" ) ) {
constraints[ "min" ] = schema[ "minimum" ];
}
if ( schema.keyExists( "maximum" ) ) {
constraints[ "max" ] = schema[ "maximum" ];
}
if ( schema.keyExists( "default" ) ) {
constraints[ "defaultValue" ] = schema[ "default" ];
}
if ( schema.keyExists( "minLength" ) || schema.keyExists( "maxLength" ) ) {
param schema.minLength = "";
param schema.maxLength = "";
constraints[ "size" ] = "#schema.minLength#..#schema.maxLength#";
}
if ( schema.keyExists( "x-coldbox-additional-validation" ) ) {
structAppend( constraints, schema[ "x-coldbox-additional-validation" ] );
}
for (
var c in [
"after",
"afterOrEqual",
"before",
"beforeOrEqual",
"dateEquals"
]
) {
if ( constraints.keyExists( c ) && constraints[ c ] == "now" ) {
constraints[ c ] = now();
}
}
return constraints;
}

private string function addValidationType( required struct constraints, required struct metadata ){
param arguments.metadata.type = "";
param arguments.metadata.format = "";
switch ( arguments.metadata.type ) {
case "integer":
arguments.constraints[ "type" ] = "integer";
break;
case "number":
switch ( arguments.metadata.format ) {
case "double":
case "float":
arguments.constraints[ "type" ] = "float";
break;
default:
arguments.constraints[ "type" ] = "numeric";
break;
}
break;
case "boolean":
arguments.constraints[ "type" ] = "boolean";
break;
case "array":
arguments.constraints[ "type" ] = "array";
break;
case "object":
arguments.constraints[ "type" ] = "struct";
break;
case "string":
switch ( arguments.metadata.format ) {
case "date-time-without-timezone":
arguments.constraints[ "type" ] = "date";
break;
default:
arguments.constraints[ "type" ] = "string";
break;
}
break;
}
}

private struct function _discoverPaths(
string parametersPath = "",
string requestBodyPath = "",
boolean discoverPaths = true,
string callingComponent,
string callingFunctionName
){
var parametersKey = "parameters";
if ( arguments.parametersPath != "" ) {
parametersKey = listLast( arguments.parametersPath, "####" );
arguments.parametersPath = reReplaceNoCase(
listFirst( arguments.parametersPath, "####" ),
"^~",
"/resources/apidocs/"
);
if ( parametersKey == "" ) {
parametersKey = "parameters";
}
}

if ( arguments.discoverPaths ) {
if ( arguments.parametersPath == "" || arguments.requestBodyPath == "" ) {
param variables.localActions = getMetadata( variables.$parent ).functions;
if ( isNull( arguments.callingFunctionName ) ) {
var stackFrames = callStackGet();
for ( var stackFrame in stackFrames ) {
if (
!arrayContains(
[
"_discoverPaths",
"generateConstraintsFromOpenAPISchema",
"getByDelegate"
],
stackFrame[ "function" ]
)
) {
arguments.callingFunctionName = stackFrame[ "function" ];
break;
}
}
}
var callingFunction = variables.localActions.filter( ( action ) => {
return action.name == callingFunctionName;
} );
if ( !callingFunction.isEmpty() ) {
if ( arguments.parametersPath == "" && callingFunction[ 1 ].keyExists( "x-parameters" ) ) {
parametersKey = listLast( callingFunction[ 1 ][ "x-parameters" ], "####" );
arguments.parametersPath = reReplaceNoCase(
listFirst( callingFunction[ 1 ][ "x-parameters" ], "####" ),
"^~",
"/resources/apidocs/"
);
}
if ( arguments.requestBodyPath == "" && callingFunction[ 1 ].keyExists( "requestBody" ) ) {
arguments.requestBodyPath = reReplaceNoCase(
listFirst( callingFunction[ 1 ][ "requestBody" ], "####" ),
"^~",
"/resources/apidocs/"
);
}
}
}
}

return {
"parametersPath" : arguments.parametersPath,
"parametersKey" : parametersKey,
"requestBodyPath" : arguments.requestBodyPath
};
}

}
49 changes: 27 additions & 22 deletions models/RoutesParser.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ component accessors="true" threadsafe singleton {

// Incorporate our API routes into the document
filterDesignatedRoutes().each( function( key, value ){
template[ "paths" ].putAll( createPathsFromRouteConfig( value ) );
structAppend(
template[ "paths" ],
createPathsFromRouteConfig( value ),
true
);
} );

// Build out the Open API Document object
Expand Down Expand Up @@ -192,7 +196,7 @@ component accessors="true" threadsafe singleton {
for ( var pathEntry in entrySet ) {
for ( var routeKey in designatedRoutes ) {
if ( replaceNoCase( routeKey, "/", "", "ALL" ) == pathEntry ) {
sortedRoutes.put( routeKey, designatedRoutes[ routeKey ] );
sortedRoutes[ routeKey ] = designatedRoutes[ routeKey ];
}
}
}
Expand Down Expand Up @@ -287,7 +291,7 @@ component accessors="true" threadsafe singleton {
// method not in error methods
if ( !arrayFindNoCase( errorMethods, actions[ methodList ] ) ) {
// Create new path template
path.put( lCase( methodName ), getOpenAPIUtil().newMethod() );
path[ lCase( methodName ) ] = getOpenAPIUtil().newMethod();
// Append Params
appendPathParams( pathKey = arguments.pathKey, method = path[ lCase( methodName ) ] );
// Append Function metadata
Expand All @@ -309,7 +313,7 @@ component accessors="true" threadsafe singleton {
} else {
for ( var methodName in getOpenAPIUtil().defaultMethods() ) {
// Insert path template for default method
path.put( lCase( methodName ), getOpenAPIUtil().newMethod() );
path[ lCase( methodName ) ] = getOpenAPIUtil().newMethod();
// Append Params
appendPathParams( pathKey = arguments.pathKey, method = path[ lCase( methodName ) ] );
// Append metadata
Expand Down Expand Up @@ -346,7 +350,7 @@ component accessors="true" threadsafe singleton {
}
}

arguments.existingPaths.put( "/" & arrayToList( pathSegments, "/" ), path );
arguments.existingPaths[ "/" & arrayToList( pathSegments, "/" ) ] = path;
}

/**
Expand All @@ -358,7 +362,7 @@ component accessors="true" threadsafe singleton {
private void function appendPathParams( required string pathKey, required struct method ){
// Verify parameters array in the method definition
if ( !structKeyExists( arguments.method, "parameters" ) ) {
arguments.method.put( "parameters", [] );
arguments.method[ "parameters" ] = [];
}

// handle any parameters in the url now
Expand Down Expand Up @@ -508,44 +512,41 @@ component accessors="true" threadsafe singleton {

// hint/description
if ( infoKey == "hint" ) {
if ( !method.containsKey( "description" ) || method[ "description" ] == "" ) {
method.put( "description", infoMetadata );
if ( !structKeyExists( method, "description" ) || method[ "description" ] == "" ) {
method[ "description" ] = infoMetadata;
}
if ( !functionMetadata.containsKey( "summary" ) ) {
method.put( "summary", infoMetadata );
if ( !structKeyExists( functionMetadata, "summary" ) ) {
method[ "summary" ] = infoMetadata;
}
continue;
}

if ( infoKey == "description" && infoMetadata != "" ) {
method.put( "description", infoMetadata );
method[ "description" ] = infoMetadata;
continue;
}

if ( infoKey == "summary" ) {
method.put( "summary", infoMetadata );
method[ "summary" ] = infoMetadata;
continue;
}

// Operation Tags
if ( infoKey == "tags" ) {
method.put(
"tags",
( isSimpleValue( infoMetadata ) ? listToArray( infoMetadata ) : infoMetadata )
);
method[ "tags" ] = isSimpleValue( infoMetadata ) ? listToArray( infoMetadata ) : infoMetadata;
continue;
}

// Request body: { description, required, content : {} } if simple, we just add it as required, with listed as content
if ( left( infoKey, 12 ) == "requestBody" ) {
method.put( "requestBody", structNew( "ordered" ) );
method[ "requestBody" ] = structNew( "ordered" );

if ( isSimpleValue( infoMetadata ) ) {
method[ "requestBody" ][ "description" ] = infoMetadata;
method[ "requestBody" ][ "required" ] = true;
method[ "requestBody" ][ "content" ] = { "#infoMetadata#" : {} };
} else {
method[ "requestBody" ].putAll( infoMetadata );
structAppend( method[ "requestBody" ], infoMetadata, true );
}
continue;
}
Expand Down Expand Up @@ -635,13 +636,13 @@ component accessors="true" threadsafe singleton {
var filterString = arrayToList(
[
moduleName,
listLast( handlerMetadata.name, "." ),
listLast( handlerMetadata.fullname, "." ),
methodName
],
"."
);
} else {
var filterString = arrayToList( [ handlerMetadata.name, methodName ], "." );
var filterString = arrayToList( [ handlerMetadata.fullname, methodName ], "." );
}

availableFiles
Expand Down Expand Up @@ -754,14 +755,18 @@ component accessors="true" threadsafe singleton {
// get reponse name
var responseName = right( infoKey, len( infoKey ) - 9 );

method[ "responses" ].put( responseName, structNew( "ordered" ) );
method[ "responses" ][ responseName ] = structNew( "ordered" );

// Use simple value for description and content type
if ( isSimpleValue( infoMetadata ) ) {
method[ "responses" ][ responseName ][ "description" ] = infoMetadata;
method[ "responses" ][ responseName ][ "content" ] = { "#infoMetadata#" : {} };
} else {
method[ "responses" ][ responseName ].putAll( infoMetadata );
structAppend(
method[ "responses" ][ responseName ],
infoMetadata,
true
);
}
} );

Expand Down
Loading
Loading