Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,7 @@ public CodegenModel fromModel(String name, Schema model) {
String dataType = toModelName(ref);
mdl.dataType = dataType;
mdl.isAlias = false;
mdl.vendorExtensions.put("x-needs-nested-validation", true);
LOGGER.debug("Schema for: " + name + " is wrapper for: " + dataType);
}

Expand Down Expand Up @@ -1331,6 +1332,19 @@ public CodegenModel fromModel(String name, Schema model) {
}
}

// Match the property validation rule for model-valued newtype fields.
boolean needsNestedValidation = (mdl.dataType != null && mdl.dataType.contains("models::"))
|| (mdl.getItems() != null && mdl.getItems().dataType != null
&& mdl.getItems().dataType.contains("models::"));
if (!mdl.oneOf.isEmpty() || !mdl.anyOf.isEmpty()) {
// Primitive alternatives do not implement Validate.
needsNestedValidation = ModelUtils.getInterfaces(model).stream()
.allMatch(member -> getTypeDeclaration(member).contains("models::"));
}
if (needsNestedValidation) {
mdl.vendorExtensions.put("x-needs-nested-validation", true);
}

mdl.vendorExtensions.put("x-upper-case-name", name.toUpperCase(Locale.ROOT));
mdl.vendorExtensions.put("x-is-string", isString);
mdl.vendorExtensions.put("x-to-string-support", toStringSupport);
Expand Down Expand Up @@ -1644,6 +1658,10 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
property.vendorExtensions.put("x-needs-nested-validation", true);
}

if (property.isArray && property.isNullable) {
property.vendorExtensions.put("x-nullable-array", true);
}

// TODO: We should avoid reverse engineering primitive type status from the data type
String strippedType = stripNullable(property.dataType);
if (!languageSpecificPrimitives.contains(strippedType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ impl std::str::FromStr for {{{classname}}} {
#[serde(rename = "{{{.}}}")]
{{/xmlName}}
pub struct {{{classname}}}(
{{>validate}} {{{dataType}}}
{{>validate}}{{^hasConflictingModelNames}}{{#exts.x-needs-nested-validation}} #[cfg_attr(feature = "validate", validate)]
{{/exts.x-needs-nested-validation}}{{/hasConflictingModelNames}} {{{dataType}}}
);

impl std::convert::From<{{{dataType}}}> for {{{classname}}} {
Expand Down Expand Up @@ -250,7 +251,8 @@ pub struct {{{classname}}}(
#[serde(serialize_with = "wrap_in_{{{x-item-xml-name}}}")]
{{/x-item-xml-name}}
{{/exts}}
Vec<{{{arrayModelType}}}>
{{^hasConflictingModelNames}}{{>validate}}{{#exts.x-needs-nested-validation}} #[cfg_attr(feature = "validate", validate)]
{{/exts.x-needs-nested-validation}}{{/hasConflictingModelNames}} Vec<{{{arrayModelType}}}>
);

impl std::convert::From<Vec<{{{arrayModelType}}}>> for {{{classname}}} {
Expand Down Expand Up @@ -419,7 +421,7 @@ pub struct {{{classname}}} {
{{^hasConflictingModelNames}}{{>validate}}{{/hasConflictingModelNames}}
{{^hasConflictingModelNames}}
{{#exts.x-needs-nested-validation}}
{{^minLength}}{{^maxLength}}{{^minItems}}{{^maxItems}}#[cfg_attr(feature = "validate", validate)]{{/maxItems}}{{/minItems}}{{/maxLength}}{{/minLength}}
#[cfg_attr(feature = "validate", validate)]
Comment thread
EricLiu2000 marked this conversation as resolved.
Resolved
{{/exts.x-needs-nested-validation}}
{{/hasConflictingModelNames}}
pub {{{name}}}: {{{dataType}}},
Expand All @@ -432,7 +434,7 @@ pub struct {{{classname}}} {
{{^hasConflictingModelNames}}{{>validate}}{{/hasConflictingModelNames}}
{{^hasConflictingModelNames}}
{{#exts.x-needs-nested-validation}}
{{^minLength}}{{^maxLength}}{{^minItems}}{{^maxItems}}#[cfg_attr(feature = "validate", validate)]{{/maxItems}}{{/minItems}}{{/maxLength}}{{/minLength}}
#[cfg_attr(feature = "validate", validate)]
{{/exts.x-needs-nested-validation}}
{{/hasConflictingModelNames}}
#[serde(skip_serializing_if="Option::is_none")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@
#[cfg_attr(feature = "validate", validate(pattern = r"{{{pattern}}}"))]
{{/isByteArray}}
{{/pattern}}
{{! Nullable arrays retain their existing bounds: the runtime has no nullable item-count traits. }}
{{#minItems}}
#[cfg_attr(feature = "validate", validate(min_length = {{{minItems}}}))]
#[cfg_attr(feature = "validate", validate({{#exts.x-nullable-array}}min_length{{/exts.x-nullable-array}}{{^exts.x-nullable-array}}min_items{{/exts.x-nullable-array}} = {{{minItems}}}))]
{{/minItems}}
{{#maxItems}}
#[cfg_attr(feature = "validate", validate(max_length = {{{maxItems}}}))]
#[cfg_attr(feature = "validate", validate({{#exts.x-nullable-array}}max_length{{/exts.x-nullable-array}}{{^exts.x-nullable-array}}max_items{{/exts.x-nullable-array}} = {{{maxItems}}}))]
{{/maxItems}}
{{#isEnum}}
{{#allowableValues}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.openapitools.codegen.rust;

import org.apache.commons.io.FileUtils;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.config.CodegenConfigurator;
Expand All @@ -18,6 +19,36 @@
*/
public class RustServerCodegenTest {

@Test
public void testRecursiveModelValidation() throws IOException {
Path target = Files.createTempDirectory("recursive-validation");
try {
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("rust-server")
.setInputSpec("src/test/resources/3_0/rust-server/recursive-validation.yaml")
.setGenerateAliasAsModel(true)
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
new DefaultGenerator().opts(configurator.toClientOptInput()).generate();

Path modelsPath = target.resolve("src/models.rs");
TestUtils.assertFileContains(modelsPath,
"validate(min_items = 1)",
"validate(max_items = 2)",
"#[cfg_attr(feature = \"validate\", validate)]\n Vec<Positive>",
"#[cfg_attr(feature = \"validate\", validate)]\n std::collections::HashMap<String, models::Positive>",
"#[cfg_attr(feature = \"validate\", validate)]\n StringValue",
"#[cfg_attr(feature = \"validate\", validate)]\n swagger::OneOf2<models::Positive,models::Parent>",
"#[cfg_attr(feature = \"validate\", validate)]\n swagger::AnyOf2<models::Positive,models::Positives>",
"#[cfg_attr(feature = \"validate\", validate)]\n swagger::OneOf2<Positive,Parent>",
"pub struct MixedChoice(\n swagger::OneOf2<models::Positive,String>",
"pub struct PrimitiveChoice(\n swagger::AnyOf2<i32,String>",
"#[cfg_attr(feature = \"validate\", validate)]\n pub children: Vec<models::Positive>",
"#[cfg_attr(feature = \"validate\", validate)]\n #[serde(skip_serializing_if=\"Option::is_none\")]\n pub optional_children: Option<Vec<models::Positive>>");
} finally {
FileUtils.deleteDirectory(target.toFile());
}
}

/**
* Test that integer parameters with minimum/maximum constraints are assigned appropriate Rust types.
* This tests that integer parameter type fitting logic is applied to CodegenParameter objects.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Generate recursive-validation.yaml with --generate-alias-as-model and
// --additional-properties packageName=recursive_validation. Copy this file to
// tests/recursive_validation.rs in the output, then run
// cargo test --no-default-features --features validate --test recursive_validation.
#![cfg(feature = "validate")]
use recursive_validation::models::{
BoundedChildren, MixedChoice, ModelAnyOf, ModelChoice, NamedModelChoice, NullableChildren,
NullableStringItems, Parent, Positive, PositiveMap, Positives, PrimitiveChoice, StringAlias,
StringItems, StringValue,
};
use serde_valid::Validate;

#[test]
fn root_validation_reaches_array_wrapper_items_and_bounds() {
for values in [vec![], vec![1], vec![1, 1], vec![1, 1, 1], vec![0]] {
let expected = (1..=2).contains(&values.len()) && values.iter().all(|value| *value >= 1);
let root = Parent {
children: Positives::from(
values
.into_iter()
.map(|value| Positive { value })
.collect::<Vec<_>>(),
),
};
assert_eq!(root.validate().is_ok(), expected);
}
}

#[test]
fn bounded_fields_validate_their_size_and_children() {
for values in [vec![], vec![1], vec![1, 1], vec![1, 1, 1], vec![0]] {
for optional in [false, true] {
let expected =
(1..=2).contains(&values.len()) && values.iter().all(|value| *value >= 1);
let children = values
.iter()
.map(|value| Positive { value: *value })
.collect();
let root = if optional {
BoundedChildren {
children: vec![Positive { value: 1 }],
optional_children: Some(children),
}
} else {
BoundedChildren {
children,
optional_children: None,
}
};
assert_eq!(root.validate().is_ok(), expected);
}
}
}

#[test]
fn primitive_array_wrappers_validate_their_size() {
for size in 0..=3 {
let value = StringItems::from(vec![String::new(); size]);
assert_eq!(value.validate().is_ok(), (1..=2).contains(&size));
}
}

#[test]
fn map_wrappers_validate_their_values() {
for value in [0, 1] {
let root = PositiveMap::from(std::collections::HashMap::from([(
"key".to_owned(),
Positive { value },
)]));
assert_eq!(root.validate().is_ok(), value == 1);
}
}

#[test]
fn reference_wrappers_validate_their_inner_model() {
for value in ["", "x"] {
let root = StringAlias::from(StringValue::from(value.to_owned()));
assert_eq!(root.validate().is_ok(), !value.is_empty());
}
}

#[test]
fn union_wrappers_compile_with_model_and_primitive_members() {
let model = ModelChoice::from(swagger::OneOf2::A(Positive { value: 1 }));
let named = NamedModelChoice::from(swagger::OneOf2::A(Positive { value: 1 }));
let any = ModelAnyOf::from(swagger::AnyOf2::B(Positives::from(vec![Positive {
value: 1,
}])));
let mixed = MixedChoice::from(swagger::OneOf2::B("future".to_owned()));
let primitive: PrimitiveChoice = serde_json::from_str("42").unwrap();
assert!(model.validate().is_ok());
assert!(named.validate().is_ok());
assert!(any.validate().is_ok());
assert!(mixed.validate().is_ok());
assert!(primitive.validate().is_ok());
}

#[test]
fn bounded_nullable_fields_keep_compiling_and_validate_present_models() {
let value = NullableChildren {
children: swagger::Nullable::Null,
optional_children: None,
};
assert!(value.validate().is_ok());
for optional in [false, true] {
for value in [0, 1] {
let children = swagger::Nullable::Present(vec![Positive { value }]);
let root = if optional {
NullableChildren {
children: swagger::Nullable::Null,
optional_children: Some(children),
}
} else {
NullableChildren {
children,
optional_children: Some(swagger::Nullable::Null),
}
};
assert_eq!(root.validate().is_ok(), value == 1);
}
}
let items = NullableStringItems::from(vec![String::new()]);
assert!(items.validate().is_ok());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
openapi: 3.0.3
info: {title: Recursive validation, version: 1.0.0}
paths:
/parents/{name}:
get:
operationId: getParent
parameters:
- name: name
in: path
required: true
schema: {type: string, pattern: '^x+$'}
responses:
'200':
description: A parent
content:
application/json:
schema: {$ref: '#/components/schemas/Parent'}
components:
schemas:
Positive:
type: object
required: [value]
properties:
value: {type: integer, minimum: 1}
Positives:
type: array
minItems: 1
maxItems: 2
items: {$ref: '#/components/schemas/Positive'}
Parent:
type: object
required: [children]
properties:
children: {$ref: '#/components/schemas/Positives'}
ModelChoice:
oneOf:
- $ref: '#/components/schemas/Positive'
- $ref: '#/components/schemas/Parent'
NamedModelChoice:
x-one-of-name: 'swagger::OneOf2<Positive,Parent>'
oneOf:
- $ref: '#/components/schemas/Positive'
- $ref: '#/components/schemas/Parent'
MixedChoice:
oneOf:
- $ref: '#/components/schemas/Positive'
- type: string
PrimitiveChoice:
anyOf:
- type: integer
- type: string
ModelAnyOf:
anyOf:
- $ref: '#/components/schemas/Positive'
- $ref: '#/components/schemas/Positives'
BoundedChildren:
type: object
required: [children]
properties:
children:
type: array
minItems: 1
maxItems: 2
items:
$ref: '#/components/schemas/Positive'
optionalChildren:
type: array
minItems: 1
maxItems: 2
items:
$ref: '#/components/schemas/Positive'
StringItems:
type: array
minItems: 1
maxItems: 2
items: {type: string}
PositiveMap:
type: object
additionalProperties: {$ref: '#/components/schemas/Positive'}
StringValue:
type: string
minLength: 1
StringAlias:
$ref: '#/components/schemas/StringValue'
NullableChildren:
type: object
required: [children]
properties:
children:
type: array
nullable: true
minItems: 1
maxItems: 2
items: {$ref: '#/components/schemas/Positive'}
optionalChildren:
type: array
nullable: true
minItems: 1
maxItems: 2
items: {$ref: '#/components/schemas/Positive'}
NullableStringItems:
type: array
nullable: true
minItems: 1
maxItems: 2
items: {type: string}
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ impl Animal {
#[cfg_attr(feature = "validate", derive(Validate))]
#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))]
pub struct AnimalFarm(
#[cfg_attr(feature = "validate", validate)]
Vec<Animal>
);

Expand Down
Loading