@@ -50,14 +50,16 @@ if Code.ensure_loaded?(OpenApiSpex) do
50
50
Tag
51
51
}
52
52
53
+ require Logger
54
+
53
55
@ typep content_type_format ( ) :: :json | :multipart
54
56
@ typep acc ( ) :: map ( )
55
57
56
58
@ doc """
57
59
Creates an empty accumulator for schema generation.
58
60
"""
59
61
def empty_acc do
60
- % { schemas: % { } , seen_non_schema_types: [ ] }
62
+ % { schemas: % { } , seen_non_schema_types: [ ] , seen_input_types: [ ] }
61
63
end
62
64
63
65
@ dialyzer { :nowarn_function , { :action_description , 3 } }
@@ -673,7 +675,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
673
675
) do
674
676
if instance_of = constraints [ :instance_of ] do
675
677
if AshJsonApi.JsonSchema . embedded? ( instance_of ) && ! constraints [ :fields ] do
676
- embedded_type_input ( attr , action_type , acc , format )
678
+ embedded_type_input ( attr , resource , action_type , acc , format )
677
679
else
678
680
{ schema , acc } =
679
681
resource_write_attribute_type (
@@ -695,7 +697,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
695
697
{ schema , acc } =
696
698
cond do
697
699
AshJsonApi.JsonSchema . embedded? ( type ) ->
698
- embedded_type_input ( attr , action_type , acc )
700
+ embedded_type_input ( attr , resource , action_type , acc )
699
701
700
702
:erlang . function_exported ( type , :json_write_schema , 1 ) ->
701
703
{ type . json_write_schema ( attr . constraints ) , acc }
@@ -947,7 +949,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
947
949
948
950
if type_key in acc . seen_non_schema_types do
949
951
# We're in a recursive loop, return $ref and warn
950
- require Logger
952
+ # Recursive type detected, using $ref instead of inline definition
951
953
952
954
Logger . warning (
953
955
"Detected recursive embedded type with JSON API type: #{ inspect ( instance_of ) } "
@@ -988,8 +990,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
988
990
989
991
if type_key in acc . seen_non_schema_types do
990
992
# We're in a recursive loop, return empty schema
991
- require Logger
992
- Logger . warning ( "Detected recursive embedded type: #{ inspect ( instance_of ) } " )
993
+ # Recursive type detected, returning empty schema to prevent infinite loop
993
994
{ % Schema { } , acc }
994
995
else
995
996
# Mark this type as seen and process it
@@ -1059,18 +1060,77 @@ if Code.ensure_loaded?(OpenApiSpex) do
1059
1060
end
1060
1061
end
1061
1062
1062
- defp embedded_type_input ( % { type: resource } = attribute , action_type , acc , format \\ :json ) do
1063
+ defp embedded_type_input (
1064
+ % { type: embedded_resource } = attribute ,
1065
+ parent_resource ,
1066
+ action_type ,
1067
+ acc ,
1068
+ format \\ :json
1069
+ ) do
1063
1070
attribute = % {
1064
1071
attribute
1065
- | constraints: Ash.Type.NewType . constraints ( resource , attribute . constraints )
1072
+ | constraints: Ash.Type.NewType . constraints ( embedded_resource , attribute . constraints )
1066
1073
}
1067
1074
1068
- resource =
1075
+ embedded_resource =
1069
1076
case attribute . constraints [ :instance_of ] do
1070
- nil -> Ash.Type.NewType . subtype_of ( resource )
1077
+ nil -> Ash.Type.NewType . subtype_of ( embedded_resource )
1071
1078
type -> type
1072
1079
end
1073
1080
1081
+ input_schema_name =
1082
+ create_input_schema_name ( attribute , parent_resource , action_type , embedded_resource )
1083
+
1084
+ type_key = { embedded_resource , action_type , attribute . constraints }
1085
+
1086
+ # Check for recursion
1087
+ if type_key in acc . seen_input_types do
1088
+ # We're in a recursive loop
1089
+ if input_schema_name do
1090
+ # Return $ref and unchanged accumulator (the schema will be created by the non-recursive path)
1091
+ schema = % { "$ref" => "#/components/schemas/#{ input_schema_name } " }
1092
+ { schema , acc }
1093
+ else
1094
+ # No schema name, return empty schema to break recursion
1095
+ { % Schema { } , acc }
1096
+ end
1097
+ else
1098
+ # Not recursive, mark as seen and process normally
1099
+ new_acc = % { acc | seen_input_types: [ type_key | acc . seen_input_types ] }
1100
+
1101
+ # Build the schema
1102
+ embedded_type_input_impl (
1103
+ attribute ,
1104
+ embedded_resource ,
1105
+ action_type ,
1106
+ new_acc ,
1107
+ format ,
1108
+ input_schema_name
1109
+ )
1110
+ end
1111
+ end
1112
+
1113
+ defp create_input_schema_name ( attribute , parent_resource , action_type , embedded_resource ) do
1114
+ # Check if this embedded resource has a JSON API type for input schema naming
1115
+ json_api_type = AshJsonApi.Resource.Info . type ( embedded_resource )
1116
+
1117
+ if json_api_type do
1118
+ "#{ json_api_type } -input-#{ action_type } "
1119
+ else
1120
+ # Use parent resource type and attribute name for schema naming
1121
+ # This matches the pattern used in the generated refs
1122
+ parent_type = AshJsonApi.Resource.Info . type ( parent_resource )
1123
+ attribute_name = Map . get ( attribute , :name )
1124
+
1125
+ if parent_type && attribute_name do
1126
+ "#{ parent_type } _#{ attribute_name } -input-#{ action_type } "
1127
+ else
1128
+ nil
1129
+ end
1130
+ end
1131
+ end
1132
+
1133
+ defp embedded_type_input_impl ( attribute , resource , action_type , acc , format , schema_name ) do
1074
1134
create_action =
1075
1135
case attribute . constraints [ :create_action ] do
1076
1136
nil ->
@@ -1145,7 +1205,15 @@ if Code.ensure_loaded?(OpenApiSpex) do
1145
1205
}
1146
1206
|> add_null_for_non_required ( )
1147
1207
1148
- { schema , acc }
1208
+ if schema_name do
1209
+ # Store the schema in the accumulator
1210
+ final_acc = % { acc | schemas: Map . put ( acc . schemas , schema_name , schema ) }
1211
+ # Return a $ref to the schema
1212
+ ref_schema = % { "$ref" => "#/components/schemas/#{ schema_name } " }
1213
+ { ref_schema , final_acc }
1214
+ else
1215
+ { schema , acc }
1216
+ end
1149
1217
end
1150
1218
1151
1219
defp unwrap_any_of ( % { "anyOf" => options } = schema ) do
@@ -1478,6 +1546,10 @@ if Code.ensure_loaded?(OpenApiSpex) do
1478
1546
{ parameters_list , acc } = parameters ( route , resource , path_params , acc )
1479
1547
{ response , acc } = response_body ( route , resource , acc )
1480
1548
1549
+ { request_body_result , request_schemas } = request_body ( route , resource )
1550
+
1551
+ acc_with_request_schemas = % { acc | schemas: Map . merge ( acc . schemas , request_schemas ) }
1552
+
1481
1553
operation = % Operation {
1482
1554
description: action_description ( action , route , resource ) ,
1483
1555
operationId: route . name ,
@@ -1489,10 +1561,10 @@ if Code.ensure_loaded?(OpenApiSpex) do
1489
1561
} ,
1490
1562
response_code => response
1491
1563
} ,
1492
- requestBody: request_body ( route , resource )
1564
+ requestBody: request_body_result
1493
1565
}
1494
1566
1495
- { operation , acc }
1567
+ { operation , acc_with_request_schemas }
1496
1568
end
1497
1569
1498
1570
defp action_description ( action , route , resource ) do
@@ -1921,7 +1993,7 @@ if Code.ensure_loaded?(OpenApiSpex) do
1921
1993
|> then ( fn { params , acc } -> { Enum . reverse ( params ) , acc } end )
1922
1994
end
1923
1995
1924
- @ spec request_body ( Route . t ( ) , resource :: module ) :: nil | RequestBody . t ( )
1996
+ @ spec request_body ( Route . t ( ) , resource :: module ) :: { nil | RequestBody . t ( ) , map ( ) }
1925
1997
defp request_body ( % { type: type } , _resource )
1926
1998
when type not in [
1927
1999
:route ,
@@ -1931,55 +2003,66 @@ if Code.ensure_loaded?(OpenApiSpex) do
1931
2003
:patch_relationship ,
1932
2004
:delete_from_relationship
1933
2005
] do
1934
- nil
2006
+ { nil , % { } }
1935
2007
end
1936
2008
1937
2009
defp request_body ( route , resource ) do
1938
- { json_body_schema , _acc } = request_body_schema ( route , resource , :json , % { } )
1939
- { multipart_body_schema , _acc } = request_body_schema ( route , resource , :multipart , % { } )
2010
+ { json_body_schema , json_acc } = request_body_schema ( route , resource , :json , empty_acc ( ) )
1940
2011
1941
- if route . type == :route &&
1942
- ( route . method == :delete || Enum . empty? ( json_body_schema . properties . data . properties ) ) do
1943
- nil
1944
- else
1945
- body_required =
1946
- cond do
1947
- route . type in [ :post_to_relationship , :delete_from_relationship , :patch_relationship ] ->
1948
- true
2012
+ { multipart_body_schema , multipart_acc } =
2013
+ request_body_schema ( route , resource , :multipart , empty_acc ( ) )
1949
2014
1950
- route . type == :route ->
1951
- json_body_schema . properties . data . required != [ ]
2015
+ all_schemas = Map . merge ( json_acc . schemas , multipart_acc . schemas )
1952
2016
1953
- true ->
1954
- json_body_schema . properties . data . properties . attributes . required != [ ] ||
1955
- json_body_schema . properties . data . properties . relationships . required != [ ]
1956
- end
2017
+ body =
2018
+ if route . type == :route &&
2019
+ ( route . method == :delete || Enum . empty? ( json_body_schema . properties . data . properties ) ) do
2020
+ nil
2021
+ else
2022
+ body_required =
2023
+ cond do
2024
+ route . type in [
2025
+ :post_to_relationship ,
2026
+ :delete_from_relationship ,
2027
+ :patch_relationship
2028
+ ] ->
2029
+ true
1957
2030
1958
- content =
1959
- if json_body_schema == multipart_body_schema do
1960
- # No file inputs declared, multipart is not necessary
1961
- % {
1962
- "application/vnd.api+json" => % MediaType { schema: json_body_schema }
1963
- }
1964
- else
1965
- % {
1966
- "application/vnd.api+json" => % MediaType { schema: json_body_schema } ,
1967
- "multipart/x.ash+form-data" => % MediaType {
1968
- schema: % Schema {
1969
- multipart_body_schema
1970
- | additionalProperties: % { type: :string , format: :binary }
2031
+ route . type == :route ->
2032
+ json_body_schema . properties . data . required != [ ]
2033
+
2034
+ true ->
2035
+ json_body_schema . properties . data . properties . attributes . required != [ ] ||
2036
+ json_body_schema . properties . data . properties . relationships . required != [ ]
2037
+ end
2038
+
2039
+ content =
2040
+ if json_body_schema == multipart_body_schema do
2041
+ # No file inputs declared, multipart is not necessary
2042
+ % {
2043
+ "application/vnd.api+json" => % MediaType { schema: json_body_schema }
2044
+ }
2045
+ else
2046
+ % {
2047
+ "application/vnd.api+json" => % MediaType { schema: json_body_schema } ,
2048
+ "multipart/x.ash+form-data" => % MediaType {
2049
+ schema: % Schema {
2050
+ multipart_body_schema
2051
+ | additionalProperties: % { type: :string , format: :binary }
2052
+ }
1971
2053
}
1972
2054
}
1973
- }
1974
- end
2055
+ end
1975
2056
1976
- % RequestBody {
1977
- description:
1978
- "Request body for the #{ route . name || route . route } operation on #{ AshJsonApi.Resource.Info . type ( resource ) } resource" ,
1979
- required: body_required ,
1980
- content: content
1981
- }
1982
- end
2057
+ % RequestBody {
2058
+ description:
2059
+ "Request body for the #{ route . name || route . route } operation on #{ AshJsonApi.Resource.Info . type ( resource ) } resource" ,
2060
+ required: body_required ,
2061
+ content: content
2062
+ }
2063
+ end
2064
+
2065
+ { body , all_schemas }
1983
2066
end
1984
2067
1985
2068
@ spec request_body_schema (
0 commit comments