Skip to content

Commit fea8604

Browse files
committed
Generate cbValidation constraints from Swagger docs
1 parent dd635f7 commit fea8604

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
component name="OpenAPIConstraintsGenerator" {
2+
3+
property name="cache" inject="cachebox:template";
4+
5+
public struct function generateConstraintsFromOpenAPISchema(
6+
string parametersPath = "",
7+
string requestBodyPath = "",
8+
boolean discoverPaths = true,
9+
string callingFunctionName
10+
){
11+
var paths = _discoverPaths( argumentCollection = arguments );
12+
13+
var constraints = {};
14+
15+
if ( paths.parametersPath != "" ) {
16+
var parametersJSON = variables.cache.getOrSet(
17+
paths.parametersPath,
18+
() => {
19+
return deserializeJSON( fileRead( paths.parametersPath ) );
20+
},
21+
createTimespan( 1, 0, 0, 0 )
22+
);
23+
structAppend( constraints, generateConstraintsFromParameters( parametersJSON[ "parameters" ] ) )
24+
}
25+
26+
if ( paths.requestBodyPath != "" ) {
27+
var requestBodyJSON = variables.cache.getOrSet(
28+
paths.requestBodyPath,
29+
() => {
30+
return deserializeJSON( fileRead( paths.requestBodyPath ) );
31+
},
32+
createTimespan( 1, 0, 0, 0 )
33+
);
34+
var schema = requestBodyJSON[ "content" ][ "application/json" ][ "schema" ];
35+
structAppend(
36+
constraints,
37+
generateConstraintsFromRequestBodyProperties( schema.properties, schema.required )
38+
);
39+
}
40+
41+
return constraints;
42+
}
43+
44+
private struct function generateConstraintsFromParameters( required array parameters ){
45+
return arguments.parameters.reduce( ( allConstraints, parameter ) => {
46+
allConstraints[ parameter.name ] = generateConstraint(
47+
schema = ( parameter.schema ?: {} ),
48+
isRequired = ( parameter.required ?: false )
49+
);
50+
return allConstraints;
51+
}, {} );
52+
}
53+
54+
private struct function generateConstraintsFromRequestBodyProperties(
55+
required struct properties,
56+
required array requiredFields
57+
){
58+
return arguments.properties.map( ( fieldName, schema ) => {
59+
return generateConstraint(
60+
schema = schema,
61+
isRequired = requiredFields.containsNoCase( fieldName ) > 0
62+
);
63+
} );
64+
}
65+
66+
private struct function generateConstraint( required struct schema, required boolean isRequired ){
67+
var constraints = {};
68+
constraints[ "required" ] = arguments.isRequired;
69+
addValidationType( constraints, schema );
70+
if ( constraints[ "type" ] == "struct" ) {
71+
constraints[ "constraints" ] = generateConstraintsFromRequestBodyProperties(
72+
schema.properties,
73+
schema.required ?: []
74+
);
75+
}
76+
if ( constraints[ "type" ] == "array" ) {
77+
constraints[ "items" ] = generateConstraint( schema.items, arguments.isRequired );
78+
}
79+
if ( schema.keyExists( "enum" ) ) {
80+
constraints[ "inList" ] = arrayToList( schema[ "enum" ] );
81+
}
82+
if ( schema.keyExists( "minimum" ) ) {
83+
constraints[ "min" ] = schema[ "minimum" ];
84+
}
85+
if ( schema.keyExists( "maximum" ) ) {
86+
constraints[ "max" ] = schema[ "maximum" ];
87+
}
88+
if ( schema.keyExists( "default" ) ) {
89+
constraints[ "defaultValue" ] = schema[ "default" ];
90+
}
91+
if ( schema.keyExists( "minLength" ) || schema.keyExists( "maxLength" ) ) {
92+
param schema.minLength = "";
93+
param schema.maxLength = "";
94+
constraints[ "size" ] = "#schema.minLength#..#schema.maxLength#";
95+
}
96+
if ( schema.keyExists( "x-coldbox-additional-validation" ) ) {
97+
structAppend( constraints, schema[ "x-coldbox-additional-validation" ] );
98+
}
99+
for (
100+
var c in [
101+
"after",
102+
"afterOrEqual",
103+
"before",
104+
"beforeOrEqual",
105+
"dateEquals"
106+
]
107+
) {
108+
if ( constraints.keyExists( c ) && constraints[ c ] == "now" ) {
109+
constraints[ c ] = now();
110+
}
111+
}
112+
return constraints;
113+
}
114+
115+
private string function addValidationType( required struct constraints, required struct metadata ){
116+
param arguments.metadata.type = "";
117+
param arguments.metadata.format = "";
118+
switch ( arguments.metadata.type ) {
119+
case "integer":
120+
arguments.constraints[ "type" ] = "integer";
121+
break;
122+
case "number":
123+
switch ( arguments.metadata.format ) {
124+
case "double":
125+
case "float":
126+
arguments.constraints[ "type" ] = "float";
127+
break;
128+
default:
129+
arguments.constraints[ "type" ] = "numeric";
130+
break;
131+
}
132+
break;
133+
case "boolean":
134+
arguments.constraints[ "type" ] = "boolean";
135+
break;
136+
case "array":
137+
arguments.constraints[ "type" ] = "array";
138+
break;
139+
case "object":
140+
arguments.constraints[ "type" ] = "struct";
141+
break;
142+
case "string":
143+
switch ( arguments.metadata.format ) {
144+
case "date-time-without-timezone":
145+
arguments.constraints[ "type" ] = "date";
146+
break;
147+
default:
148+
arguments.constraints[ "type" ] = "string";
149+
break;
150+
}
151+
break;
152+
}
153+
}
154+
155+
private struct function _discoverPaths(
156+
string parametersPath = "",
157+
string requestBodyPath = "",
158+
boolean discoverPaths = true,
159+
string callingComponent,
160+
string callingFunctionName
161+
){
162+
if ( arguments.discoverPaths ) {
163+
if ( arguments.parametersPath == "" || arguments.requestBodyPath == "" ) {
164+
param variables.localActions = getMetadata( variables.$parent ).functions;
165+
if ( isNull( arguments.callingFunctionName ) ) {
166+
var stackFrames = callStackGet();
167+
for ( var stackFrame in stackFrames ) {
168+
if (
169+
!arrayContains(
170+
[
171+
"_discoverPaths",
172+
"generateConstraintsFromOpenAPISchema",
173+
"getByDelegate"
174+
],
175+
stackFrame[ "function" ]
176+
)
177+
) {
178+
arguments.callingFunctionName = stackFrame[ "function" ];
179+
break;
180+
}
181+
}
182+
}
183+
var callingFunction = variables.localActions.filter( ( action ) => {
184+
return action.name == callingFunctionName;
185+
} );
186+
if ( !callingFunction.isEmpty() ) {
187+
if ( arguments.parametersPath == "" && callingFunction[ 1 ].keyExists( "x-parameters" ) ) {
188+
arguments.parametersPath = reReplaceNoCase(
189+
listFirst( callingFunction[ 1 ][ "x-parameters" ], "####" ),
190+
"^~",
191+
"/resources/apidocs/"
192+
);
193+
}
194+
if ( arguments.requestBodyPath == "" && callingFunction[ 1 ].keyExists( "requestBody" ) ) {
195+
arguments.requestBodyPath = reReplaceNoCase(
196+
listFirst( callingFunction[ 1 ][ "requestBody" ], "####" ),
197+
"^~",
198+
"/resources/apidocs/"
199+
);
200+
}
201+
}
202+
}
203+
}
204+
205+
return {
206+
"parametersPath" : arguments.parametersPath,
207+
"requestBodyPath" : arguments.requestBodyPath
208+
};
209+
}
210+
211+
}

readme.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,48 @@ component displayName="API.v1.Users"{
295295
}
296296
```
297297

298+
## Integration with cbValidation
299+
300+
You can utilize your Swagger docs to generate constraints to use with cbValidation. You can do so by utilizing
301+
`OpenAPIConstraintsGenerator@cbSwagger` as a delegate. Doing so exposes a `generateConstraintsFromOpenAPISchema` function
302+
that will generate the constraints.
303+
304+
```
305+
component delegates="OpenAPIConstraintsGenerator@cbSwagger" {
306+
307+
/**
308+
* @route (GET) /api/v1/jokes
309+
*
310+
* Returns a random dad joke
311+
*
312+
* @x-parameters ~api/v1/Jokes/index/parameters.json##parameters
313+
* @requestBody ~api/v1/Jokes/index/requestBody.json
314+
* @response-200 ~api/v1/Jokes/index/responses.200.json
315+
* @response-403 ~api/v1/errors/example.403.json
316+
*/
317+
function index( event, rc, prc ) {
318+
var validated = validateOrFail( target = rc, constraints = generateConstraintsFromOpenAPISchema() );
319+
// ...
320+
}
321+
322+
}
323+
```
324+
325+
Using `OpenAPIConstraintsGenerator@cbSwagger` as a delegate allows it to auto discover the parameters and
326+
request body for the action using the annotations on your action.
327+
328+
If you prefer to manually pass in the parameters and/or requestBody path or use the `OpenAPIConstraintsGenerator@cbSwagger`
329+
as an injection, you can pass in the paths as arguments to the `generateConstraintsFromOpenAPISchema` function.
330+
331+
```
332+
public struct function generateConstraintsFromOpenAPISchema(
333+
string parametersPath = "",
334+
string requestBodyPath = "",
335+
boolean discoverPaths = true, // this only discovers paths not explicitly passed in
336+
string callingFunctionName // this will look itself up using the current call stack
337+
)
338+
```
339+
298340
********************************************************************************
299341
Copyright Since 2016 Ortus Solutions, Corp
300342
www.ortussolutions.com

0 commit comments

Comments
 (0)