fix: wrong typespec generation for all-of with single ref (#21139)

This commit is contained in:
Enrique Fernández 2025-04-25 10:06:06 +02:00 committed by GitHub
parent d02c0f493e
commit 0462bed734
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 169 additions and 154 deletions

View File

@ -45,19 +45,18 @@ These options may be applied as additional-properties (cli) or configOptions (pl
## LANGUAGE PRIMITIVES ## LANGUAGE PRIMITIVES
<ul class="column-ul"> <ul class="column-ul">
<li>AnyType</li> <li>Date.t</li>
<li>Atom</li> <li>DateTime.t</li>
<li>Boolean</li> <li>String.t</li>
<li>Decimal</li>
<li>Float</li>
<li>Integer</li>
<li>List</li>
<li>Map</li>
<li>PID</li>
<li>String</li>
<li>Tuple</li>
<li>any()</li> <li>any()</li>
<li>binary()</li>
<li>boolean()</li>
<li>float()</li>
<li>integer()</li>
<li>list()</li>
<li>map()</li> <li>map()</li>
<li>nil</li>
<li>number()</li>
</ul> </ul>
## RESERVED WORDS ## RESERVED WORDS

View File

@ -180,44 +180,55 @@ public class ElixirClientCodegen extends DefaultCodegen {
*/ */
languageSpecificPrimitives = new HashSet<>( languageSpecificPrimitives = new HashSet<>(
Arrays.asList( Arrays.asList(
"Integer", "integer()",
"Float", "float()",
"Decimal", "number()",
"Boolean", "boolean()",
"String", "String.t",
"List", "Date.t",
"Atom", "DateTime.t",
"Map", "binary()",
"AnyType", "list()",
"Tuple",
"PID",
// This is a workaround, since the DefaultCodeGen uses our elixir TypeSpec
// datetype to evaluate the primitive
"map()", "map()",
"any()")); "any()",
"nil"));
// ref: // ref:
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types
typeMapping = new HashMap<>(); typeMapping = new HashMap<>();
typeMapping.put("integer", "Integer"); // primitive types
typeMapping.put("long", "Integer"); typeMapping.put("string", "String.t");
typeMapping.put("number", "Float"); typeMapping.put("number", "number()");
typeMapping.put("float", "Float"); typeMapping.put("integer", "integer()");
typeMapping.put("double", "Float"); typeMapping.put("boolean", "boolean()");
typeMapping.put("string", "String"); typeMapping.put("array", "list()");
typeMapping.put("byte", "Integer"); typeMapping.put("object", "map()");
typeMapping.put("boolean", "Boolean"); typeMapping.put("map", "map()");
typeMapping.put("Date", "Date"); typeMapping.put("null", "nil");
typeMapping.put("DateTime", "DateTime"); // string formats
typeMapping.put("file", "String"); typeMapping.put("byte", "String.t");
typeMapping.put("map", "Map"); typeMapping.put("binary", "binary()");
typeMapping.put("array", "List"); typeMapping.put("password", "String.t");
typeMapping.put("list", "List"); typeMapping.put("uuid", "String.t");
typeMapping.put("object", "Map"); typeMapping.put("email", "String.t");
typeMapping.put("binary", "String"); typeMapping.put("uri", "String.t");
typeMapping.put("ByteArray", "String"); typeMapping.put("file", "String.t");
typeMapping.put("UUID", "String"); // integer formats
typeMapping.put("URI", "String"); typeMapping.put("int32", "integer()");
typeMapping.put("int64", "integer()");
typeMapping.put("long", "integer()");
// float formats
typeMapping.put("float", "float()");
typeMapping.put("double", "float()");
typeMapping.put("decimal", "float()");
// date-time formats
typeMapping.put("date", "Date.t");
typeMapping.put("date-time", "DateTime.t");
// other
typeMapping.put("ByteArray", "binary()");
typeMapping.put("DateTime", "DateTime.t");
typeMapping.put("UUID", "String.t");
cliOptions.add(new CliOption(CodegenConstants.INVOKER_PACKAGE, cliOptions.add(new CliOption(CodegenConstants.INVOKER_PACKAGE,
"The main namespace to use for all classes. e.g. Yay.Pets")); "The main namespace to use for all classes. e.g. Yay.Pets"));
@ -570,49 +581,19 @@ public class ElixirClientCodegen extends DefaultCodegen {
*/ */
@Override @Override
public String getTypeDeclaration(Schema p) { public String getTypeDeclaration(Schema p) {
if (ModelUtils.isAnyType(p)) { if (ModelUtils.isArraySchema(p)) {
return "any()";
} else if(ModelUtils.isFreeFormObject(p, null)) {
return "%{optional(String.t) => any()}";
} else if (ModelUtils.isArraySchema(p)) {
Schema inner = ModelUtils.getSchemaItems(p); Schema inner = ModelUtils.getSchemaItems(p);
return "[" + getTypeDeclaration(inner) + "]"; return "[" + getTypeDeclaration(inner) + "]";
} else if (ModelUtils.isMapSchema(p)) { } else if (ModelUtils.isMapSchema(p)) {
Schema inner = ModelUtils.getAdditionalProperties(p); Schema inner = ModelUtils.getAdditionalProperties(p);
return "%{optional(String.t) => " + getTypeDeclaration(inner) + "}"; return "%{optional(String.t) => " + getTypeDeclaration(inner) + "}";
} else if (ModelUtils.isPasswordSchema(p)) {
return "String.t";
} else if (ModelUtils.isEmailSchema(p)) {
return "String.t";
} else if (ModelUtils.isByteArraySchema(p)) {
return "binary()";
} else if (ModelUtils.isUUIDSchema(p)) {
return "String.t";
} else if (ModelUtils.isDateSchema(p)) {
return "Date.t";
} else if (ModelUtils.isDateTimeSchema(p)) {
return "DateTime.t";
} else if (ModelUtils.isObjectSchema(p)) {
return "map()";
} else if (ModelUtils.isIntegerSchema(p)) {
return "integer()";
} else if (ModelUtils.isNumberSchema(p)) {
return "float()";
} else if (ModelUtils.isBinarySchema(p) || ModelUtils.isFileSchema(p)) {
return "String.t";
} else if (ModelUtils.isBooleanSchema(p)) {
return "boolean()";
} else if (!StringUtils.isEmpty(p.get$ref())) { } else if (!StringUtils.isEmpty(p.get$ref())) {
switch (super.getTypeDeclaration(p)) { String refType = super.getTypeDeclaration(p);
case "String": if (languageSpecificPrimitives.contains(refType)) {
return "String.t"; return refType;
default: } else {
return this.moduleName + ".Model." + super.getTypeDeclaration(p) + ".t"; return this.moduleName + ".Model." + refType + ".t";
} }
} else if (ModelUtils.isFileSchema(p)) {
return "String.t";
} else if (ModelUtils.isStringSchema(p)) {
return "String.t";
} else if (p.getType() == null) { } else if (p.getType() == null) {
return "any()"; return "any()";
} }
@ -630,14 +611,11 @@ public class ElixirClientCodegen extends DefaultCodegen {
@Override @Override
public String getSchemaType(Schema p) { public String getSchemaType(Schema p) {
String openAPIType = super.getSchemaType(p); String openAPIType = super.getSchemaType(p);
String type = null;
if (typeMapping.containsKey(openAPIType)) { if (typeMapping.containsKey(openAPIType)) {
type = typeMapping.get(openAPIType); return typeMapping.get(openAPIType);
if (languageSpecificPrimitives.contains(type)) } else {
return toModelName(type); return toModelName(openAPIType);
} else }
type = openAPIType;
return toModelName(type);
} }
class ExtendedCodegenResponse extends CodegenResponse { class ExtendedCodegenResponse extends CodegenResponse {
@ -784,24 +762,6 @@ public class ElixirClientCodegen extends DefaultCodegen {
this.operationIdCamelCase = o.operationIdCamelCase; this.operationIdCamelCase = o.operationIdCamelCase;
} }
private void translateBaseType(StringBuilder returnEntry, String baseType) {
switch (baseType) {
case "AnyType":
returnEntry.append("any()");
break;
case "Boolean":
returnEntry.append("boolean()");
break;
case "Float":
returnEntry.append("float()");
break;
default:
returnEntry.append(baseType);
returnEntry.append(".t");
break;
}
}
public String typespec() { public String typespec() {
StringBuilder sb = new StringBuilder("@spec "); StringBuilder sb = new StringBuilder("@spec ");
sb.append(underscore(operationId)); sb.append(underscore(operationId));
@ -819,16 +779,10 @@ public class ElixirClientCodegen extends DefaultCodegen {
for (CodegenResponse response : this.responses) { for (CodegenResponse response : this.responses) {
ExtendedCodegenResponse exResponse = (ExtendedCodegenResponse) response; ExtendedCodegenResponse exResponse = (ExtendedCodegenResponse) response;
StringBuilder returnEntry = new StringBuilder(); StringBuilder returnEntry = new StringBuilder();
if (exResponse.baseType == null) { if (exResponse.schema != null) {
returnEntry.append("nil"); returnEntry.append(getTypeDeclaration((Schema) exResponse.schema));
} else if (exResponse.containerType == null) { // not container (array, map, set)
returnEntry.append(normalizeTypeName(exResponse.dataType, exResponse.primitiveType));
} else { } else {
if (exResponse.containerType.equals("array") || exResponse.containerType.equals("set")) { returnEntry.append(normalizeTypeName(exResponse.dataType, exResponse.primitiveType));
returnEntry.append(exResponse.dataType);
} else if (exResponse.containerType.equals("map")) {
returnEntry.append("map()");
}
} }
uniqueResponseTypes.add(returnEntry.toString()); uniqueResponseTypes.add(returnEntry.toString());
} }
@ -845,14 +799,11 @@ public class ElixirClientCodegen extends DefaultCodegen {
if (baseType == null) { if (baseType == null) {
return "nil"; return "nil";
} }
if (isPrimitive || "String.t".equals(baseType)) { if (isPrimitive || languageSpecificPrimitives.contains(baseType)) {
return baseType; return baseType;
} }
if (!baseType.startsWith(moduleName + ".Model.")) { if (!baseType.startsWith(moduleName + ".Model.")) {
baseType = moduleName + ".Model." + baseType; baseType = moduleName + ".Model." + baseType + ".t";
}
if (!baseType.endsWith(".t")) {
baseType += ".t";
} }
return baseType; return baseType;
} }

View File

@ -1337,6 +1337,29 @@ paths:
responses: responses:
200: 200:
description: The instance started successfully description: The instance started successfully
/fake/all-of-with-local-single-ref:
get:
tags:
- fake
responses:
200:
description: Successful operation
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Foo"
/fake/all-of-with-remote-single-ref:
get:
tags:
- fake
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/AllOfWithSingleRef"
servers: servers:
- url: "http://{server}.swagger.io:{port}/v2" - url: "http://{server}.swagger.io:{port}/v2"
description: petstore server description: petstore server

View File

@ -9,6 +9,60 @@ defmodule OpenapiPetstore.Api.Fake do
alias OpenapiPetstore.Connection alias OpenapiPetstore.Connection
import OpenapiPetstore.RequestBuilder import OpenapiPetstore.RequestBuilder
@doc """
### Parameters
- `connection` (OpenapiPetstore.Connection): Connection to server
- `opts` (keyword): Optional parameters
### Returns
- `{:ok, OpenapiPetstore.Model.Foo.t}` on success
- `{:error, Tesla.Env.t}` on failure
"""
@spec fake_all_of_with_local_single_ref_get(Tesla.Env.client, keyword()) :: {:ok, any()} | {:error, Tesla.Env.t}
def fake_all_of_with_local_single_ref_get(connection, _opts \\ []) do
request =
%{}
|> method(:get)
|> url("/fake/all-of-with-local-single-ref")
|> Enum.into([])
connection
|> Connection.request(request)
|> evaluate_response([
{200, OpenapiPetstore.Model.Foo}
])
end
@doc """
### Parameters
- `connection` (OpenapiPetstore.Connection): Connection to server
- `opts` (keyword): Optional parameters
### Returns
- `{:ok, OpenapiPetstore.Model.AllOfWithSingleRef.t}` on success
- `{:error, Tesla.Env.t}` on failure
"""
@spec fake_all_of_with_remote_single_ref_get(Tesla.Env.client, keyword()) :: {:ok, OpenapiPetstore.Model.AllOfWithSingleRef.t} | {:error, Tesla.Env.t}
def fake_all_of_with_remote_single_ref_get(connection, _opts \\ []) do
request =
%{}
|> method(:get)
|> url("/fake/all-of-with-remote-single-ref")
|> Enum.into([])
connection
|> Connection.request(request)
|> evaluate_response([
{200, OpenapiPetstore.Model.AllOfWithSingleRef}
])
end
@doc """ @doc """
for Java apache and Java native, test toUrlQueryString for maps with BegDecimal keys for Java apache and Java native, test toUrlQueryString for maps with BegDecimal keys
@ -180,14 +234,14 @@ defmodule OpenapiPetstore.Api.Fake do
- `connection` (OpenapiPetstore.Connection): Connection to server - `connection` (OpenapiPetstore.Connection): Connection to server
- `opts` (keyword): Optional parameters - `opts` (keyword): Optional parameters
- `:body` (float()): Input number as post body - `:body` (number()): Input number as post body
### Returns ### Returns
- `{:ok, float()}` on success - `{:ok, number()}` on success
- `{:error, Tesla.Env.t}` on failure - `{:error, Tesla.Env.t}` on failure
""" """
@spec fake_outer_number_serialize(Tesla.Env.client, keyword()) :: {:ok, float()} | {:error, Tesla.Env.t} @spec fake_outer_number_serialize(Tesla.Env.client, keyword()) :: {:ok, number()} | {:error, Tesla.Env.t}
def fake_outer_number_serialize(connection, opts \\ []) do def fake_outer_number_serialize(connection, opts \\ []) do
optional_params = %{ optional_params = %{
:body => :body :body => :body
@ -464,7 +518,7 @@ defmodule OpenapiPetstore.Api.Fake do
### Parameters ### Parameters
- `connection` (OpenapiPetstore.Connection): Connection to server - `connection` (OpenapiPetstore.Connection): Connection to server
- `number` (float()): None - `number` (number()): None
- `double` (float()): None - `double` (float()): None
- `pattern_without_delimiter` (String.t): None - `pattern_without_delimiter` (String.t): None
- `byte` (binary()): None - `byte` (binary()): None
@ -485,7 +539,7 @@ defmodule OpenapiPetstore.Api.Fake do
- `{:ok, nil}` on success - `{:ok, nil}` on success
- `{:error, Tesla.Env.t}` on failure - `{:error, Tesla.Env.t}` on failure
""" """
@spec test_endpoint_parameters(Tesla.Env.client, float(), float(), String.t, binary(), keyword()) :: {:ok, nil} | {:error, Tesla.Env.t} @spec test_endpoint_parameters(Tesla.Env.client, number(), float(), String.t, binary(), keyword()) :: {:ok, nil} | {:error, Tesla.Env.t}
def test_endpoint_parameters(connection, number, double, pattern_without_delimiter, byte, opts \\ []) do def test_endpoint_parameters(connection, number, double, pattern_without_delimiter, byte, opts \\ []) do
optional_params = %{ optional_params = %{
:integer => :form, :integer => :form,
@ -623,7 +677,7 @@ defmodule OpenapiPetstore.Api.Fake do
### Parameters ### Parameters
- `connection` (OpenapiPetstore.Connection): Connection to server - `connection` (OpenapiPetstore.Connection): Connection to server
- `body` (%{optional(String.t) => any()}): request body - `body` (map()): request body
- `opts` (keyword): Optional parameters - `opts` (keyword): Optional parameters
### Returns ### Returns

View File

@ -54,7 +54,7 @@ defmodule OpenapiPetstore.Api.Store do
- `{:ok, %{}}` on success - `{:ok, %{}}` on success
- `{:error, Tesla.Env.t}` on failure - `{:error, Tesla.Env.t}` on failure
""" """
@spec get_inventory(Tesla.Env.client, keyword()) :: {:ok, map()} | {:error, Tesla.Env.t} @spec get_inventory(Tesla.Env.client, keyword()) :: {:ok, %{optional(String.t) => integer()}} | {:error, Tesla.Env.t}
def get_inventory(connection, _opts \\ []) do def get_inventory(connection, _opts \\ []) do
request = request =
%{} %{}

View File

@ -12,7 +12,7 @@ defmodule OpenapiPetstore.Model.ArrayOfArrayOfNumberOnly do
] ]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
:ArrayArrayNumber => [[float()]] | nil :ArrayArrayNumber => [[number()]] | nil
} }
def decode(value) do def decode(value) do

View File

@ -12,7 +12,7 @@ defmodule OpenapiPetstore.Model.ArrayOfNumberOnly do
] ]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
:ArrayNumber => [float()] | nil :ArrayNumber => [number()] | nil
} }
def decode(value) do def decode(value) do

View File

@ -13,8 +13,8 @@ defmodule OpenapiPetstore.Model.FakeBigDecimalMap200Response do
] ]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
:someId => float() | nil, :someId => number() | nil,
:someMap => %{optional(String.t) => float()} | nil :someMap => %{optional(String.t) => number()} | nil
} }
def decode(value) do def decode(value) do

View File

@ -30,10 +30,10 @@ defmodule OpenapiPetstore.Model.FormatTest do
:integer => integer() | nil, :integer => integer() | nil,
:int32 => integer() | nil, :int32 => integer() | nil,
:int64 => integer() | nil, :int64 => integer() | nil,
:number => float(), :number => number(),
:float => float() | nil, :float => float() | nil,
:double => float() | nil, :double => float() | nil,
:decimal => String.t | nil, :decimal => float() | nil,
:string => String.t | nil, :string => String.t | nil,
:byte => binary(), :byte => binary(),
:binary => String.t | nil, :binary => String.t | nil,
@ -45,12 +45,8 @@ defmodule OpenapiPetstore.Model.FormatTest do
:pattern_with_digits_and_delimiter => String.t | nil :pattern_with_digits_and_delimiter => String.t | nil
} }
alias OpenapiPetstore.Deserializer
def decode(value) do def decode(value) do
value value
|> Deserializer.deserialize(:date, :date, nil)
|> Deserializer.deserialize(:dateTime, :datetime, nil)
end end
end end

View File

@ -23,7 +23,6 @@ defmodule OpenapiPetstore.Model.MixedPropertiesAndAdditionalPropertiesClass do
def decode(value) do def decode(value) do
value value
|> Deserializer.deserialize(:dateTime, :datetime, nil)
|> Deserializer.deserialize(:map, :map, OpenapiPetstore.Model.Animal) |> Deserializer.deserialize(:map, :map, OpenapiPetstore.Model.Animal)
end end
end end

View File

@ -24,25 +24,21 @@ defmodule OpenapiPetstore.Model.NullableClass do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
:integer_prop => integer() | nil, :integer_prop => integer() | nil,
:number_prop => float() | nil, :number_prop => number() | nil,
:boolean_prop => boolean() | nil, :boolean_prop => boolean() | nil,
:string_prop => String.t | nil, :string_prop => String.t | nil,
:date_prop => Date.t | nil, :date_prop => Date.t | nil,
:datetime_prop => DateTime.t | nil, :datetime_prop => DateTime.t | nil,
:array_nullable_prop => [%{optional(String.t) => any()}] | nil, :array_nullable_prop => [map()] | nil,
:array_and_items_nullable_prop => [%{optional(String.t) => any()}] | nil, :array_and_items_nullable_prop => [map()] | nil,
:array_items_nullable => [%{optional(String.t) => any()}] | nil, :array_items_nullable => [map()] | nil,
:object_nullable_prop => %{optional(String.t) => any()} | nil, :object_nullable_prop => %{optional(String.t) => map()} | nil,
:object_and_items_nullable_prop => %{optional(String.t) => any()} | nil, :object_and_items_nullable_prop => %{optional(String.t) => map()} | nil,
:object_items_nullable => %{optional(String.t) => any()} | nil :object_items_nullable => %{optional(String.t) => map()} | nil
} }
alias OpenapiPetstore.Deserializer
def decode(value) do def decode(value) do
value value
|> Deserializer.deserialize(:date_prop, :date, nil)
|> Deserializer.deserialize(:datetime_prop, :datetime, nil)
end end
end end

View File

@ -12,7 +12,7 @@ defmodule OpenapiPetstore.Model.NumberOnly do
] ]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
:JustNumber => float() | nil :JustNumber => number() | nil
} }
def decode(value) do def decode(value) do

View File

@ -16,7 +16,7 @@ defmodule OpenapiPetstore.Model.ObjectWithDeprecatedFields do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
:uuid => String.t | nil, :uuid => String.t | nil,
:id => float() | nil, :id => number() | nil,
:deprecatedRef => OpenapiPetstore.Model.DeprecatedModel.t | nil, :deprecatedRef => OpenapiPetstore.Model.DeprecatedModel.t | nil,
:bars => [String.t] | nil :bars => [String.t] | nil
} }

View File

@ -25,11 +25,8 @@ defmodule OpenapiPetstore.Model.Order do
:complete => boolean() | nil :complete => boolean() | nil
} }
alias OpenapiPetstore.Deserializer
def decode(value) do def decode(value) do
value value
|> Deserializer.deserialize(:shipDate, :datetime, nil)
end end
end end

View File

@ -14,7 +14,7 @@ defmodule OpenapiPetstore.Model.OuterComposite do
] ]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
:my_number => float() | nil, :my_number => number() | nil,
:my_string => String.t | nil, :my_string => String.t | nil,
:my_boolean => boolean() | nil :my_boolean => boolean() | nil
} }