Skip to content

Commit f639216

Browse files
committed
feat: Add serde_validate support.
In addition to existing rust-server validation support.
1 parent a9f439f commit f639216

61 files changed

Lines changed: 1941 additions & 150 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/samples-rust-server.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ jobs:
6060
cargo build --bin ${package##*/} --features cli
6161
target/debug/${package##*/} --help
6262
fi
63+
# Test the validate feature if it exists
64+
if cargo read-manifest | grep -q '"validate"'; then
65+
cargo build --features validate --all-targets
66+
fi
6367
cargo fmt
6468
cargo test
6569
cargo clippy

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5359,7 +5359,6 @@ public CodegenParameter fromParameter(Parameter parameter, Set<String> imports)
53595359
} else {
53605360
parameterSchema = null;
53615361
}
5362-
53635362
if (parameter instanceof QueryParameter || "query".equalsIgnoreCase(parameter.getIn())) {
53645363
codegenParameter.isQueryParam = true;
53655364
codegenParameter.isAllowEmptyValue = parameter.getAllowEmptyValue() != null && parameter.getAllowEmptyValue();
@@ -7546,6 +7545,35 @@ public CodegenParameter fromFormProperty(String name, Schema propertySchema, Set
75467545
imports.add(codegenProperty.complexType);
75477546
}
75487547

7548+
// // validation
7549+
// // handle maximum, minimum properly for int/long by removing the trailing ".0"
7550+
// if (ModelUtils.isIntegerSchema(propertySchema)) {
7551+
// codegenParameter.maximum = propertySchema.getMaximum() == null ? null : String.valueOf(propertySchema.getMaximum().toBigInteger());
7552+
// codegenParameter.minimum = propertySchema.getMinimum() == null ? null : String.valueOf(propertySchema.getMinimum().toBigInteger());
7553+
// } else {
7554+
// codegenParameter.maximum = propertySchema.getMaximum() == null ? null : String.valueOf(propertySchema.getMaximum());
7555+
// codegenParameter.minimum = propertySchema.getMinimum() == null ? null : String.valueOf(propertySchema.getMinimum());
7556+
// }
7557+
7558+
// codegenParameter.exclusiveMaximum = propertySchema.getExclusiveMaximum() == null ? false : propertySchema.getExclusiveMaximum();
7559+
// codegenParameter.exclusiveMinimum = propertySchema.getExclusiveMinimum() == null ? false : propertySchema.getExclusiveMinimum();
7560+
// codegenParameter.maxLength = propertySchema.getMaxLength();
7561+
// codegenParameter.minLength = propertySchema.getMinLength();
7562+
// codegenParameter.pattern = toRegularExpression(propertySchema.getPattern());
7563+
// codegenParameter.maxItems = propertySchema.getMaxItems();
7564+
// codegenParameter.minItems = propertySchema.getMinItems();
7565+
// codegenParameter.uniqueItems = propertySchema.getUniqueItems() == null ? false : propertySchema.getUniqueItems();
7566+
// codegenParameter.multipleOf = propertySchema.getMultipleOf();
7567+
7568+
// // exclusive* are noop without corresponding min/max
7569+
// if (codegenParameter.maximum != null || codegenParameter.minimum != null ||
7570+
// codegenParameter.maxLength != null || codegenParameter.minLength != null ||
7571+
// codegenParameter.maxItems != null || codegenParameter.minItems != null ||
7572+
// codegenParameter.pattern != null || codegenParameter.multipleOf != null) {
7573+
// codegenParameter.hasValidation = true;
7574+
// }
7575+
7576+
// setParameterBooleanFlagWithCodegenProperty(codegenParameter, codegenProperty);
75497577
// set example value
75507578
setParameterExampleValue(codegenParameter);
75517579

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public class RustServerCodegen extends AbstractRustCodegen implements CodegenCon
8585
// RFC 7807 Support
8686
private static final String problemJsonMimeType = "application/problem+json";
8787
private static final String problemXmlMimeType = "application/problem+xml";
88+
89+
// Track if we have models with conflicting names (Ok/Err) that conflict with serde_valid
90+
private boolean hasConflictingModelNames = false;
8891

8992
public RustServerCodegen() {
9093
super();
@@ -941,6 +944,11 @@ private void postProcessOperationWithModels(CodegenOperation op, List<ModelMap>
941944
if (param.contentType != null && isMimetypeJson(param.contentType)) {
942945
param.vendorExtensions.put("x-consumes-json", true);
943946
}
947+
948+
// Add a vendor extension to flag if this can have validate() run on it.
949+
if (!param.isUuid && !param.isPrimitiveType && !param.isEnum && (!param.isContainer || !languageSpecificPrimitives.contains(typeMapping.get(param.baseType)))) {
950+
param.vendorExtensions.put("x-can-validate", true);
951+
}
944952
}
945953

946954
for (CodegenParameter param : op.formParams) {
@@ -1108,6 +1116,22 @@ public String getTypeDeclaration(Schema p) {
11081116
type = super.getTypeDeclaration(p);
11091117
}
11101118

1119+
// The above misses one case where an AllOf is used but no reference is available via get$ref() because the allOf is within a `properties`.
1120+
// It's easier to do this here to take advantage of work already done in the case of AnyOf and OneOf.
1121+
if (ModelUtils.isComposedSchema(p) && StringUtils.isEmpty(p.get$ref())) {
1122+
ComposedSchema cs = (ComposedSchema) p;
1123+
if (cs.getAllOf() != null){
1124+
// Only add the models on if the referenced schema in the allOf is actually a model.
1125+
if (ModelUtils.isModel(ModelUtils.getReferencedSchema(openAPI, cs.getAllOf().get(0))))
1126+
{
1127+
LOGGER.info("adding models:: to AllOf");
1128+
type = super.getTypeDeclaration(p);
1129+
type = "models::" + type;
1130+
LOGGER.debug("Returning " + type + " from ref");
1131+
}
1132+
}
1133+
}
1134+
11111135
// We are using extrinsic nullability, rather than intrinsic, so we need to dig into the inner
11121136
// layer of the referenced schema.
11131137
Schema rp = ModelUtils.getReferencedSchema(openAPI, p);
@@ -1454,8 +1478,20 @@ public String toAllOfName(List<String> names, Schema composedSchema) {
14541478
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
14551479
super.postProcessModelProperty(model, property);
14561480

1481+
// Check for reserved field names that conflict with serde_valid macro internals
1482+
if ("ok".equalsIgnoreCase(property.name) || "err".equalsIgnoreCase(property.name)) {
1483+
model.vendorExtensions.put("x-skip-serde-valid", true);
1484+
}
1485+
1486+
// Mark properties that reference complex types (models) for nested validation
1487+
// Only add nested validation for types that reference generated models (contain "models::")
1488+
if (property.dataType != null && property.dataType.contains("models::")) {
1489+
property.vendorExtensions.put("x-needs-nested-validation", true);
1490+
}
1491+
14571492
// TODO: We should avoid reverse engineering primitive type status from the data type
1458-
if (!languageSpecificPrimitives.contains(stripNullable(property.dataType))) {
1493+
String strippedType = stripNullable(property.dataType);
1494+
if (!languageSpecificPrimitives.contains(strippedType)) {
14591495
// If we use a more qualified model name, then only camelize the actual type, not the qualifier.
14601496
if (property.dataType.contains(":")) {
14611497
int position = property.dataType.lastIndexOf(":");
@@ -1528,7 +1564,32 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
15281564

15291565
@Override
15301566
public ModelsMap postProcessModels(ModelsMap objs) {
1531-
return super.postProcessModelsEnum(objs);
1567+
ModelsMap result = super.postProcessModelsEnum(objs);
1568+
1569+
// Check for model names that conflict with serde_valid macro internals
1570+
// Once we find one, set a class-level flag that persists across all model batches
1571+
if (!hasConflictingModelNames) {
1572+
for (ModelMap modelMap : result.getModels()) {
1573+
CodegenModel model = modelMap.getModel();
1574+
if ("Ok".equalsIgnoreCase(model.classname) || "Err".equalsIgnoreCase(model.classname)) {
1575+
hasConflictingModelNames = true;
1576+
additionalProperties.put("hasConflictingModelNames", true);
1577+
break;
1578+
}
1579+
}
1580+
}
1581+
1582+
// If there are conflicting names (detected in any batch), skip serde_valid for ALL models
1583+
if (hasConflictingModelNames) {
1584+
for (ModelMap modelMap : result.getModels()) {
1585+
CodegenModel model = modelMap.getModel();
1586+
model.vendorExtensions.put("x-skip-serde-valid", true);
1587+
}
1588+
// Set the flag for this batch's template context
1589+
result.put("hasConflictingModelNames", true);
1590+
}
1591+
1592+
return result;
15321593
}
15331594

15341595
private void processParam(CodegenParameter param, CodegenOperation op) {
@@ -1613,6 +1674,11 @@ private void processParam(CodegenParameter param, CodegenOperation op) {
16131674
String exampleString = (example != null) ? "Some(" + example + ")" : "None";
16141675
param.vendorExtensions.put("x-example", exampleString);
16151676
}
1677+
1678+
// Add a vendor extension to flag if this can have validate() run on it.
1679+
if (!param.isUuid && !param.isPrimitiveType && !param.isEnum && (!param.isContainer || !languageSpecificPrimitives.contains(typeMapping.get(param.baseType)))) {
1680+
param.vendorExtensions.put("x-can-validate", true);
1681+
}
16161682
}
16171683

16181684
@Override

modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ cli = [
7272
conversion = ["frunk", "frunk_derives", "frunk_core", "frunk-enum-core", "frunk-enum-derive"]
7373

7474
mock = ["mockall"]
75+
validate = [{{^apiUsesByteArray}}"regex",{{/apiUsesByteArray}} "serde_valid", "swagger/serdevalid"]
7576

7677
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))'.dependencies]
7778
native-tls = { version = "0.2", optional = true }
@@ -100,6 +101,8 @@ regex = "1.12"
100101

101102
serde = { version = "1.0", features = ["derive"] }
102103
serde_json = "1.0"
104+
serde_valid = { version = "0.16", optional = true }
105+
103106
validator = { version = "0.20", features = ["derive"] }
104107

105108
# Crates included if required by the API definition

modules/openapi-generator/src/main/resources/rust-server/README.mustache

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ The generated library has a few optional features that can be activated through
130130
* This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types.
131131
* `cli`
132132
* This defaults to disabled and is required for building the included CLI tool.
133+
* `validate`
134+
* This defaults to disabled and allows JSON Schema validation of received data using `MakeService::set_validation` or `Service::set_validation`.
135+
* Note, enabling validation will have a performance penalty, especially if the API heavily uses regex based checks.
133136

134137
See https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section for how to use features in your `Cargo.toml`.
135138

modules/openapi-generator/src/main/resources/rust-server/models.mustache

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
#![allow(unused_qualifications)]
2+
{{^hasConflictingModelNames}}
3+
#[cfg(not(feature = "validate"))]
4+
use validator::Validate;
25

6+
use crate::models;
7+
#[cfg(any(feature = "client", feature = "server"))]
8+
use crate::header;
9+
#[cfg(feature = "validate")]
10+
use serde_valid::Validate;
11+
{{/hasConflictingModelNames}}
12+
{{#hasConflictingModelNames}}
313
use validator::Validate;
414

515
use crate::models;
616
#[cfg(any(feature = "client", feature = "server"))]
717
use crate::header;
18+
{{/hasConflictingModelNames}}
819
{{! Don't "use" structs here - they can conflict with the names of models, and mean that the code won't compile }}
920
{{#models}}
1021
{{#model}}
@@ -19,6 +30,7 @@ use crate::header;
1930
#[allow(non_camel_case_types)]
2031
#[repr(C)]
2132
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, Hash)]
33+
{{^hasConflictingModelNames}}{{^exts.x-skip-serde-valid}}#[cfg_attr(feature = "validate", derive(Validate))]{{/exts.x-skip-serde-valid}}{{/hasConflictingModelNames}}
2234
#[cfg_attr(feature = "conversion", derive(frunk_enum_derive::LabelledGenericEnum))]{{#xmlName}}
2335
#[serde(rename = "{{{.}}}")]{{/xmlName}}
2436
pub enum {{{classname}}} {
@@ -60,11 +72,14 @@ impl std::str::FromStr for {{{classname}}} {
6072
{{^isEnum}}
6173
{{#dataType}}
6274
#[derive(Debug, Clone, PartialEq, {{#exts.x-partial-ord}}PartialOrd, {{/exts.x-partial-ord}}serde::Serialize, serde::Deserialize)]
75+
{{^hasConflictingModelNames}}{{^exts.x-skip-serde-valid}}#[cfg_attr(feature = "validate", derive(Validate))]{{/exts.x-skip-serde-valid}}{{/hasConflictingModelNames}}
6376
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
6477
{{#xmlName}}
6578
#[serde(rename = "{{{.}}}")]
6679
{{/xmlName}}
67-
pub struct {{{classname}}}({{{dataType}}});
80+
pub struct {{{classname}}}(
81+
{{>validate}} {{{dataType}}}
82+
);
6883
6984
impl std::convert::From<{{{dataType}}}> for {{{classname}}} {
7085
fn from(x: {{{dataType}}}) -> Self {
@@ -176,6 +191,7 @@ where
176191
{{/exts}}
177192
{{! vec}}
178193
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
194+
{{^hasConflictingModelNames}}{{^exts.x-skip-serde-valid}}#[cfg_attr(feature = "validate", derive(Validate))]{{/exts.x-skip-serde-valid}}{{/hasConflictingModelNames}}
179195
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
180196
pub struct {{{classname}}}(
181197
{{#exts}}
@@ -272,7 +288,7 @@ impl std::str::FromStr for {{{classname}}} {
272288
{{/arrayModelType}}
273289
{{^arrayModelType}}
274290
{{! general struct}}
275-
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)]
291+
#[derive(Debug, Clone, PartialEq, Validate, serde::Serialize, serde::Deserialize)]
276292
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
277293
{{#xmlName}}
278294
#[serde(rename = "{{{.}}}")]
@@ -288,7 +304,12 @@ pub struct {{{classname}}} {
288304
{{/x-item-xml-name}}
289305
{{/exts}}
290306
{{#hasValidation}}
307+
{{^hasConflictingModelNames}}
308+
#[cfg_attr(not(feature = "validate"), validate(
309+
{{/hasConflictingModelNames}}
310+
{{#hasConflictingModelNames}}
291311
#[validate(
312+
{{/hasConflictingModelNames}}
292313
{{#maxLength}}
293314
{{#minLength}}
294315
length(min = {{minLength}}, max = {{maxLength}}),
@@ -336,8 +357,19 @@ pub struct {{{classname}}} {
336357
length(min = {{minItems}}),
337358
{{/minItems}}
338359
{{/maxItems}}
360+
{{^hasConflictingModelNames}}
361+
))]
362+
{{/hasConflictingModelNames}}
363+
{{#hasConflictingModelNames}}
339364
)]
365+
{{/hasConflictingModelNames}}
340366
{{/hasValidation}}
367+
{{^hasConflictingModelNames}}{{>validate}}{{/hasConflictingModelNames}}
368+
{{^hasConflictingModelNames}}
369+
{{#exts.x-needs-nested-validation}}
370+
#[cfg_attr(feature = "validate", validate)]
371+
{{/exts.x-needs-nested-validation}}
372+
{{/hasConflictingModelNames}}
341373
{{#required}}
342374
pub {{{name}}}: {{{dataType}}},
343375
{{/required}}
@@ -346,6 +378,11 @@ pub struct {{{classname}}} {
346378
#[serde(deserialize_with = "swagger::nullable_format::deserialize_optional_nullable")]
347379
#[serde(default = "swagger::nullable_format::default_optional_nullable")]
348380
{{/isNullable}}
381+
{{^hasConflictingModelNames}}
382+
{{#exts.x-needs-nested-validation}}
383+
#[cfg_attr(feature = "validate", validate)]
384+
{{/exts.x-needs-nested-validation}}
385+
{{/hasConflictingModelNames}}
349386
#[serde(skip_serializing_if="Option::is_none")]
350387
pub {{{name}}}: Option<{{{dataType}}}>,
351388
{{/required}}
@@ -365,6 +402,7 @@ lazy_static::lazy_static! {
365402
lazy_static::lazy_static! {
366403
static ref RE_{{#lambda.uppercase}}{{{classname}}}_{{{name}}}{{/lambda.uppercase}}: regex::bytes::Regex = regex::bytes::Regex::new(r"{{ pattern }}").unwrap();
367404
}
405+
#[cfg(not(feature = "validate"))]
368406
fn validate_byte_{{#lambda.lowercase}}{{{classname}}}_{{{name}}}{{/lambda.lowercase}}(
369407
b: &swagger::ByteArray
370408
) -> Result<(), validator::ValidationError> {

modules/openapi-generator/src/main/resources/rust-server/server-imports.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use http_body_util::{combinators::BoxBody, Full};
44
use hyper::{body::{Body, Incoming}, HeaderMap, Request, Response, StatusCode};
55
use hyper::header::{HeaderName, HeaderValue, CONTENT_TYPE};
66
use log::warn;
7+
#[cfg(feature = "validate")]
8+
use serde_valid::Validate;
79
#[allow(unused_imports)]
810
use std::convert::{TryFrom, TryInto};
911
use std::{convert::Infallible, error::Error};

modules/openapi-generator/src/main/resources/rust-server/server-make-service.mustache

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ where
99
multipart_form_size_limit: Option<u64>,
1010
{{/apiUsesMultipartFormData}}
1111
marker: PhantomData<C>,
12+
validation: bool
1213
}
1314

1415
impl<T, C> MakeService<T, C>
@@ -22,7 +23,8 @@ where
2223
{{#apiUsesMultipartFormData}}
2324
multipart_form_size_limit: Some(8 * 1024 * 1024),
2425
{{/apiUsesMultipartFormData}}
25-
marker: PhantomData
26+
marker: PhantomData,
27+
validation: false
2628
}
2729
}
2830
{{#apiUsesMultipartFormData}}
@@ -37,6 +39,12 @@ where
3739
self
3840
}
3941
{{/apiUsesMultipartFormData}}
42+
43+
// Turn on/off validation for the service being made.
44+
#[cfg(feature = "validate")]
45+
pub fn set_validation(&mut self, validation: bool) {
46+
self.validation = validation;
47+
}
4048
}
4149

4250
impl<T, C> Clone for MakeService<T, C>
@@ -51,6 +59,7 @@ where
5159
multipart_form_size_limit: Some(8 * 1024 * 1024),
5260
{{/apiUsesMultipartFormData}}
5361
marker: PhantomData,
62+
validation: self.validation
5463
}
5564
}
5665
}
@@ -65,10 +74,8 @@ where
6574
type Future = future::Ready<Result<Self::Response, Self::Error>>;
6675
6776
fn call(&self, target: Target) -> Self::Future {
68-
let service = Service::new(self.api_impl.clone()){{^apiUsesMultipartFormData}};{{/apiUsesMultipartFormData}}
69-
{{#apiUsesMultipartFormData}}
70-
.multipart_form_size_limit(self.multipart_form_size_limit);
71-
{{/apiUsesMultipartFormData}}
77+
let service = Service::new(self.api_impl.clone(), self.validation){{^apiUsesMultipartFormData}};{{/apiUsesMultipartFormData}}{{#apiUsesMultipartFormData}}
78+
.multipart_form_size_limit(self.multipart_form_size_limit);{{/apiUsesMultipartFormData}}
7279

7380
future::ok(service)
7481
}

modules/openapi-generator/src/main/resources/rust-server/server-operation.mustache

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@
187187
.expect("Unable to create Bad Request response for missing query parameter {{{baseName}}}")),
188188
};
189189
{{/required}}
190+
{{#exts.x-can-validate}}
191+
#[cfg(not(feature = "validate"))]
192+
run_validation!(param_{{{paramName}}}, "{{{baseName}}}", validation);
193+
{{/exts.x-can-validate}}
190194
{{/isArray}}
191195
{{#-last}}
192196

modules/openapi-generator/src/main/resources/rust-server/server-request-body-basic.mustache

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@
5757
.expect("Unable to create Bad Request response for missing body parameter {{{baseName}}}")),
5858
};
5959
{{/required}}
60+
{{#exts.x-can-validate}}
61+
#[cfg(not(feature = "validate"))]
62+
run_validation!(param_{{{paramName}}}, "{{{baseName}}}", validation);
63+
{{/exts.x-can-validate}}

0 commit comments

Comments
 (0)