Skip to content

Commit 9abd69f

Browse files
committed
fix(csharp): limit public oneOf constructors to operation inputs
1 parent 0c2d8e8 commit 9abd69f

191 files changed

Lines changed: 687 additions & 468 deletions

File tree

Some content is hidden

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
494494
public static final String X_MODIFIERS = "x-modifiers";
495495
public static final String X_MODIFIER_PREFIX = "x-modifier-";
496496
public static final String X_MODEL_IS_MUTABLE = "x-model-is-mutable";
497+
public static final String X_MODEL_IS_OPERATION_INPUT = "x-model-is-operation-input";
497498
public static final String X_IMPLEMENTS = "x-implements";
498499
public static final String X_IS_ONE_OF_INTERFACE = "x-is-one-of-interface";
499500
public static final String USE_ENUM_VALUE_INTERFACE = "useEnumValueInterface";

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
import com.samskivert.mustache.Mustache;
2222
import com.samskivert.mustache.Mustache.Lambda;
2323
import com.samskivert.mustache.Template;
24+
import io.swagger.v3.oas.models.Operation;
25+
import io.swagger.v3.oas.models.PathItem;
26+
import io.swagger.v3.oas.models.media.Content;
2427
import io.swagger.v3.oas.models.media.Schema;
28+
import io.swagger.v3.oas.models.parameters.Parameter;
29+
import io.swagger.v3.oas.models.parameters.RequestBody;
2530
import lombok.Getter;
2631
import lombok.Setter;
2732
import org.apache.commons.io.FilenameUtils;
@@ -608,6 +613,7 @@ public ModelsMap postProcessModels(ModelsMap objs) {
608613
@Override
609614
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
610615
final Map<String, ModelsMap> processed = super.postProcessAllModels(objs);
616+
final Set<String> operationInputModels = getOperationInputModels();
611617

612618
Map<String, CodegenModel> enumRefs = new HashMap<>();
613619
for (Map.Entry<String, ModelsMap> entry : processed.entrySet()) {
@@ -637,6 +643,9 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
637643
continue;
638644
}
639645

646+
if (operationInputModels.contains(entry.getKey()) || operationInputModels.contains(model.schemaName)) {
647+
model.vendorExtensions.put(X_MODEL_IS_OPERATION_INPUT, true);
648+
}
640649
model.vendorExtensions.put(X_MODEL_IS_MUTABLE, modelIsMutable(model, null));
641650

642651
CodegenComposedSchemas composedSchemas = model.getComposedSchemas();
@@ -710,6 +719,77 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
710719
return processed;
711720
}
712721

722+
private Set<String> getOperationInputModels() {
723+
Set<String> operationInputModels = new HashSet<>();
724+
Set<String> visitedModels = new HashSet<>();
725+
if (openAPI == null || openAPI.getPaths() == null) {
726+
return operationInputModels;
727+
}
728+
729+
for (PathItem pathItem : openAPI.getPaths().values()) {
730+
collectOperationInputModels(pathItem.getParameters(), operationInputModels, visitedModels);
731+
for (Operation operation : pathItem.readOperations()) {
732+
collectOperationInputModels(operation.getParameters(), operationInputModels, visitedModels);
733+
RequestBody requestBody = ModelUtils.getReferencedRequestBody(openAPI, operation.getRequestBody());
734+
if (requestBody != null) {
735+
collectOperationInputModels(requestBody.getContent(), operationInputModels, visitedModels);
736+
}
737+
}
738+
}
739+
return operationInputModels;
740+
}
741+
742+
private void collectOperationInputModels(List<Parameter> parameters, Set<String> operationInputModels, Set<String> visitedModels) {
743+
if (parameters == null) {
744+
return;
745+
}
746+
for (Parameter unresolvedParameter : parameters) {
747+
Parameter parameter = ModelUtils.getReferencedParameter(openAPI, unresolvedParameter);
748+
if (parameter != null) {
749+
collectOperationInputModels(parameter.getSchema(), operationInputModels, visitedModels);
750+
collectOperationInputModels(parameter.getContent(), operationInputModels, visitedModels);
751+
}
752+
}
753+
}
754+
755+
private void collectOperationInputModels(Content content, Set<String> operationInputModels, Set<String> visitedModels) {
756+
if (content == null) {
757+
return;
758+
}
759+
content.values().forEach(mediaType -> collectOperationInputModels(mediaType.getSchema(), operationInputModels, visitedModels));
760+
}
761+
762+
private void collectOperationInputModels(Schema schema, Set<String> operationInputModels, Set<String> visitedModels) {
763+
if (schema == null || Boolean.TRUE.equals(schema.getReadOnly())) {
764+
return;
765+
}
766+
if (schema.get$ref() != null) {
767+
String modelName = ModelUtils.getSimpleRef(schema.get$ref());
768+
operationInputModels.add(modelName);
769+
if (visitedModels.add(modelName)) {
770+
collectOperationInputModels(ModelUtils.getSchema(openAPI, modelName), operationInputModels, visitedModels);
771+
}
772+
}
773+
if (schema.getOneOf() != null) {
774+
schema.getOneOf().forEach(child -> collectOperationInputModels((Schema) child, operationInputModels, visitedModels));
775+
}
776+
if (schema.getAllOf() != null) {
777+
schema.getAllOf().forEach(child -> collectOperationInputModels((Schema) child, operationInputModels, visitedModels));
778+
}
779+
if (schema.getAnyOf() != null) {
780+
schema.getAnyOf().forEach(child -> collectOperationInputModels((Schema) child, operationInputModels, visitedModels));
781+
}
782+
// TODO: Traverse OAS 3.1 schema keywords that can reference operation-input models,
783+
// such as prefixItems, contains, if/then/else, dependentSchemas, and unevaluatedProperties.
784+
collectOperationInputModels(ModelUtils.getSchemaItems(schema), operationInputModels, visitedModels); // in case schema has array type
785+
if (schema.getAdditionalProperties() instanceof Schema) {
786+
collectOperationInputModels((Schema) schema.getAdditionalProperties(), operationInputModels, visitedModels);
787+
}
788+
if (schema.getProperties() != null) {
789+
schema.getProperties().values().forEach(property -> collectOperationInputModels((Schema) property, operationInputModels, visitedModels));
790+
}
791+
}
792+
713793
/**
714794
* Returns true if the model contains any properties with a public setter
715795
* If true, the model's constructor accessor should be made public to ensure end users

‎modules/openapi-generator/src/main/resources/csharp/libraries/generichost/modelGeneric.mustache‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
/// <param name="{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}">{{description}}{{^description}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/description}}{{#defaultValue}} (default to {{.}}){{/defaultValue}}</param>
1818
{{/isDiscriminator}}
1919
{{/allVars}}
20-
{{#model.vendorExtensions.x-model-is-mutable}}{{>visibility}}{{/model.vendorExtensions.x-model-is-mutable}}{{^model.vendorExtensions.x-model-is-mutable}}internal{{/model.vendorExtensions.x-model-is-mutable}} {{classname}}({{#lambda.joinWithComma}}{{{dataType}}} {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}} {{#model.composedSchemas.anyOf}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalProperty}}{{^required}}>{{/required}} {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{baseType}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}} {{/model.composedSchemas.anyOf}}{{>ModelSignature}}{{/lambda.joinWithComma}}){{#parent}} : base({{#lambda.joinWithComma}}{{#parentModel.composedSchemas.oneOf}}{{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{parent}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}}.{{#lambda.titlecase}}{{baseType}}{{/lambda.titlecase}} {{/parentModel.composedSchemas.oneOf}}{{>ModelBaseSignature}}{{/lambda.joinWithComma}}){{/parent}}
20+
{{#model.vendorExtensions.x-model-is-operation-input}}{{>visibility}}{{/model.vendorExtensions.x-model-is-operation-input}}{{^model.vendorExtensions.x-model-is-operation-input}}internal{{/model.vendorExtensions.x-model-is-operation-input}} {{classname}}({{#lambda.joinWithComma}}{{{dataType}}} {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}} {{#model.composedSchemas.anyOf}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalProperty}}{{^required}}>{{/required}} {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{baseType}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}} {{/model.composedSchemas.anyOf}}{{>ModelSignature}}{{/lambda.joinWithComma}}){{#parent}} : base({{#lambda.joinWithComma}}{{#parentModel.composedSchemas.oneOf}}{{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{parent}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}}.{{#lambda.titlecase}}{{baseType}}{{/lambda.titlecase}} {{/parentModel.composedSchemas.oneOf}}{{>ModelBaseSignature}}{{/lambda.joinWithComma}}){{/parent}}
2121
{
2222
{{#composedSchemas.anyOf}}
2323
{{#lambda.titlecase}}{{name}}{{/lambda.titlecase}}{{^required}}Option{{/required}} = {{#lambda.escape_reserved_word}}{{#lambda.camel_case}}{{name}}{{/lambda.camel_case}}{{/lambda.escape_reserved_word}};

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,42 @@
4040

4141
public class CSharpClientCodegenTest {
4242

43+
@Test
44+
public void testGenericHostOneOfConstructorsRespectApiVisibility() throws IOException {
45+
Map<String, File> publicModels = generateIssue23046Models(false);
46+
File publicModel = publicModels.get("DynamicMetadataValue");
47+
assertFileContains(publicModel.toPath(),
48+
"public DynamicMetadataValue(string @string)",
49+
"public DynamicMetadataValue(decimal @decimal)",
50+
"public DynamicMetadataValue(bool @bool)",
51+
"public DynamicMetadataValue(DateTime dateTime)");
52+
assertFileNotContains(publicModel.toPath(), "internal DynamicMetadataValue(");
53+
assertFileContains(publicModels.get("ResponseOnlyValue").toPath(),
54+
"internal ResponseOnlyValue(string @string)",
55+
"internal ResponseOnlyValue(int @int)");
56+
assertFileNotContains(publicModels.get("ResponseOnlyValue").toPath(), "public ResponseOnlyValue(");
57+
assertFileContains(publicModels.get("QueryParameterValue").toPath(),
58+
"public QueryParameterValue(string @string)",
59+
"public QueryParameterValue(int @int)");
60+
assertFileNotContains(publicModels.get("QueryParameterValue").toPath(), "internal QueryParameterValue(");
61+
assertFileContains(publicModels.get("ReadOnlyValue").toPath(),
62+
"internal ReadOnlyValue(string @string)",
63+
"internal ReadOnlyValue(int @int)");
64+
assertFileNotContains(publicModels.get("ReadOnlyValue").toPath(), "public ReadOnlyValue(");
65+
assertFileContains(publicModels.get("ForbiddenValue").toPath(),
66+
"internal ForbiddenValue(string @string)",
67+
"internal ForbiddenValue(int @int)");
68+
assertFileNotContains(publicModels.get("ForbiddenValue").toPath(), "public ForbiddenValue(");
69+
70+
File internalModel = generateIssue23046Models(true).get("DynamicMetadataValue");
71+
assertFileContains(internalModel.toPath(),
72+
"internal DynamicMetadataValue(string @string)",
73+
"internal DynamicMetadataValue(decimal @decimal)",
74+
"internal DynamicMetadataValue(bool @bool)",
75+
"internal DynamicMetadataValue(DateTime dateTime)");
76+
assertFileNotContains(internalModel.toPath(), "public DynamicMetadataValue(");
77+
}
78+
4379
@Test
4480
public void testGenericHostNullableEnumDeserializesPresentNull() throws IOException {
4581
// For a required + nullable enum, the generated generichost JsonConverter must assign
@@ -293,4 +329,37 @@ private List<String> getNames(List<CodegenProperty> props) {
293329
if (props == null) return null;
294330
return props.stream().map(v -> v.name).collect(Collectors.toList());
295331
}
332+
333+
private Map<String, File> generateIssue23046Models(boolean nonPublicApi) throws IOException {
334+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
335+
output.deleteOnExit();
336+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/csharp/issue_23046.yaml");
337+
final DefaultGenerator defaultGenerator = new DefaultGenerator();
338+
final ClientOptInput clientOptInput = new ClientOptInput();
339+
clientOptInput.openAPI(openAPI);
340+
CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen();
341+
cSharpClientCodegen.setLibrary("generichost");
342+
cSharpClientCodegen.setOutputDir(output.getAbsolutePath());
343+
cSharpClientCodegen.setNonPublicApi(nonPublicApi);
344+
clientOptInput.config(cSharpClientCodegen);
345+
defaultGenerator.opts(clientOptInput);
346+
347+
Map<String, File> files = defaultGenerator.generate().stream()
348+
.collect(Collectors.toMap(File::getPath, Function.identity()));
349+
Map<String, File> models = Map.of(
350+
"DynamicMetadataValue", getGeneratedModel(files, output, "DynamicMetadataValue"),
351+
"ResponseOnlyValue", getGeneratedModel(files, output, "ResponseOnlyValue"),
352+
"QueryParameterValue", getGeneratedModel(files, output, "QueryParameterValue"),
353+
"ReadOnlyValue", getGeneratedModel(files, output, "ReadOnlyValue"),
354+
"ForbiddenValue", getGeneratedModel(files, output, "ForbiddenValue"));
355+
return models;
356+
}
357+
358+
private File getGeneratedModel(Map<String, File> files, File output, String modelName) {
359+
String path = Paths.get(output.getAbsolutePath(),
360+
"src", "Org.OpenAPITools", "Model", modelName + ".cs").toString();
361+
File model = files.get(path);
362+
assertNotNull(model, "Could not find generated model: " + path);
363+
return model;
364+
}
296365
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Minimal repro
4+
version: 1.0.0
5+
paths:
6+
/upload:
7+
post:
8+
requestBody:
9+
content:
10+
application/json:
11+
schema:
12+
$ref: '#/components/schemas/InitiateUploadRequest'
13+
responses:
14+
'200':
15+
description: OK
16+
content:
17+
application/json:
18+
schema:
19+
$ref: '#/components/schemas/ResponseOnlyValue'
20+
/search:
21+
get:
22+
parameters:
23+
- name: filter
24+
in: query
25+
schema:
26+
$ref: '#/components/schemas/QueryParameterValue'
27+
responses:
28+
'204':
29+
description: No content
30+
components:
31+
schemas:
32+
InitiateUploadRequest:
33+
type: object
34+
properties:
35+
dynamicMetadata:
36+
type: object
37+
additionalProperties:
38+
$ref: '#/components/schemas/DynamicMetadataValue'
39+
readOnlyMetadata:
40+
type: object
41+
readOnly: true
42+
additionalProperties:
43+
$ref: '#/components/schemas/ReadOnlyValue'
44+
allowedValue:
45+
not:
46+
$ref: '#/components/schemas/ForbiddenValue'
47+
DynamicMetadataValue:
48+
oneOf:
49+
- type: string
50+
- type: number
51+
- type: boolean
52+
- type: string
53+
format: date-time
54+
ResponseOnlyValue:
55+
oneOf:
56+
- type: string
57+
- type: integer
58+
QueryParameterValue:
59+
oneOf:
60+
- type: string
61+
- type: integer
62+
ReadOnlyValue:
63+
oneOf:
64+
- type: string
65+
- type: integer
66+
ForbiddenValue:
67+
oneOf:
68+
- type: string
69+
- type: integer

‎samples/client/petstore/csharp/generichost/latest/OneOfList/src/Org.OpenAPITools/Model/OneOfArrayRequest.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public partial class OneOfArrayRequest : IValidatableObject
3434
/// Initializes a new instance of the <see cref="OneOfArrayRequest" /> class.
3535
/// </summary>
3636
/// <param name="list"></param>
37-
internal OneOfArrayRequest(List<string> list)
37+
public OneOfArrayRequest(List<string> list)
3838
{
3939
List = list;
4040
OnCreated();
@@ -44,7 +44,7 @@ internal OneOfArrayRequest(List<string> list)
4444
/// Initializes a new instance of the <see cref="OneOfArrayRequest" /> class.
4545
/// </summary>
4646
/// <param name="list1"></param>
47-
internal OneOfArrayRequest(List<TestObject> list1)
47+
public OneOfArrayRequest(List<TestObject> list1)
4848
{
4949
List1 = list1;
5050
OnCreated();

‎samples/client/petstore/csharp/generichost/latest/UseDateTimeOffset/src/Org.OpenAPITools/Model/Fruit.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public partial class Fruit : IValidatableObject
3535
/// </summary>
3636
/// <param name="apple"></param>
3737
/// <param name="color">color</param>
38-
public Fruit(Apple apple, Option<string?> color = default)
38+
internal Fruit(Apple apple, Option<string?> color = default)
3939
{
4040
Apple = apple;
4141
ColorOption = color;
@@ -47,7 +47,7 @@ public Fruit(Apple apple, Option<string?> color = default)
4747
/// </summary>
4848
/// <param name="banana"></param>
4949
/// <param name="color">color</param>
50-
public Fruit(Banana banana, Option<string?> color = default)
50+
internal Fruit(Banana banana, Option<string?> color = default)
5151
{
5252
Banana = banana;
5353
ColorOption = color;

‎samples/client/petstore/csharp/generichost/latest/UseDateTimeOffset/src/Org.OpenAPITools/Model/FruitReq.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public partial class FruitReq : IValidatableObject
3434
/// Initializes a new instance of the <see cref="FruitReq" /> class.
3535
/// </summary>
3636
/// <param name="appleReq"></param>
37-
public FruitReq(AppleReq appleReq)
37+
internal FruitReq(AppleReq appleReq)
3838
{
3939
AppleReq = appleReq;
4040
OnCreated();
@@ -44,7 +44,7 @@ public FruitReq(AppleReq appleReq)
4444
/// Initializes a new instance of the <see cref="FruitReq" /> class.
4545
/// </summary>
4646
/// <param name="bananaReq"></param>
47-
public FruitReq(BananaReq bananaReq)
47+
internal FruitReq(BananaReq bananaReq)
4848
{
4949
BananaReq = bananaReq;
5050
OnCreated();

‎samples/client/petstore/csharp/generichost/latest/UseDateTimeOffset/src/Org.OpenAPITools/Model/Mammal.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public partial class Mammal : IValidatableObject
3434
/// Initializes a new instance of the <see cref="Mammal" /> class.
3535
/// </summary>
3636
/// <param name="whale"></param>
37-
public Mammal(Whale whale)
37+
internal Mammal(Whale whale)
3838
{
3939
Whale = whale;
4040
OnCreated();
@@ -44,7 +44,7 @@ public Mammal(Whale whale)
4444
/// Initializes a new instance of the <see cref="Mammal" /> class.
4545
/// </summary>
4646
/// <param name="zebra"></param>
47-
public Mammal(Zebra zebra)
47+
internal Mammal(Zebra zebra)
4848
{
4949
Zebra = zebra;
5050
OnCreated();
@@ -54,7 +54,7 @@ public Mammal(Zebra zebra)
5454
/// Initializes a new instance of the <see cref="Mammal" /> class.
5555
/// </summary>
5656
/// <param name="pig"></param>
57-
public Mammal(Pig pig)
57+
internal Mammal(Pig pig)
5858
{
5959
Pig = pig;
6060
OnCreated();

0 commit comments

Comments
 (0)