-module(oaspec@codegen@validate). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/oaspec/codegen/validate.gleam"). -export([errors_only/1, warnings_only/1, error_to_string/1, validate/1]). -export_type([severity/0, target/0, validation_error/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -type severity() :: severity_error | severity_warning. -type target() :: target_both | target_client | target_server. -type validation_error() :: {validation_error, binary(), binary(), severity(), target()}. -file("src/oaspec/codegen/validate.gleam", 53). ?DOC(" Filter to only errors (not warnings).\n"). -spec errors_only(list(validation_error())) -> list(validation_error()). errors_only(Issues) -> gleam@list:filter( Issues, fun(E) -> erlang:element(4, E) =:= severity_error end ). -file("src/oaspec/codegen/validate.gleam", 58). ?DOC(" Filter to only warnings (not errors).\n"). -spec warnings_only(list(validation_error())) -> list(validation_error()). warnings_only(Issues) -> gleam@list:filter( Issues, fun(E) -> erlang:element(4, E) =:= severity_warning end ). -file("src/oaspec/codegen/validate.gleam", 63). ?DOC(" Convert a validation error to a human-readable string.\n"). -spec error_to_string(validation_error()) -> binary(). error_to_string(Error) -> Prefix = case erlang:element(4, Error) of severity_error -> <<"Error"/utf8>>; severity_warning -> <<"Warning"/utf8>> end, <<<<<<<>/binary, (erlang:element(2, Error))/binary>>/binary, ": "/utf8>>/binary, (erlang:element(3, Error))/binary>>. -file("src/oaspec/codegen/validate.gleam", 120). ?DOC(" Extract parameter names from path template, e.g. \"/items/{id}\" -> [\"id\"].\n"). -spec extract_path_template_names(binary()) -> list(binary()). extract_path_template_names(Path) -> Re@1 = case gleam@regexp:from_string(<<"\\{([^}]+)\\}"/utf8>>) of {ok, Re} -> Re; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"oaspec/codegen/validate"/utf8>>, function => <<"extract_path_template_names"/utf8>>, line => 121, value => _assert_fail, start => 4038, 'end' => 4093, pattern_start => 4049, pattern_end => 4055}) end, _pipe = gleam@regexp:scan(Re@1, Path), gleam@list:filter_map(_pipe, fun(Match) -> case erlang:element(3, Match) of [{some, Name}] -> {ok, Name}; _ -> {error, nil} end end). -file("src/oaspec/codegen/validate.gleam", 88). ?DOC( " Validate that all {param} templates in the path have a corresponding\n" " path parameter definition. Reports unbound templates that would produce\n" " invalid generated code with literal {param} in URLs.\n" ). -spec validate_path_template_params( binary(), binary(), list(oaspec@openapi@spec:parameter()) ) -> list(validation_error()). validate_path_template_params(Op_id, Path, Params) -> Template_names = extract_path_template_names(Path), Path_param_names = gleam@list:filter_map( Params, fun(P) -> case erlang:element(3, P) of in_path -> {ok, erlang:element(2, P)}; _ -> {error, nil} end end ), gleam@list:filter_map( Template_names, fun(Name) -> case gleam@list:contains(Path_param_names, Name) of true -> {error, nil}; false -> {ok, {validation_error, <>, <<<<<<<<"Path template parameter '{"/utf8, Name/binary>>/binary, "}' in '"/utf8>>/binary, Path/binary>>/binary, "' has no corresponding parameter definition."/utf8>>, severity_error, target_both}} end end ). -file("src/oaspec/codegen/validate.gleam", 241). -spec resolve_schema_object( gleam@option:option(oaspec@openapi@schema:schema_ref()), oaspec@codegen@context:context() ) -> gleam@option:option(oaspec@openapi@schema:schema_object()). resolve_schema_object(Schema_ref, Ctx) -> case Schema_ref of {some, {inline, Schema_obj}} -> {some, Schema_obj}; {some, Schema_ref@1} -> case oaspec@openapi@resolver:resolve_schema_ref( Schema_ref@1, erlang:element(2, Ctx) ) of {ok, Schema_obj@1} -> {some, Schema_obj@1}; {error, _} -> none end; none -> none end. -file("src/oaspec/codegen/validate.gleam", 212). ?DOC(" Validate that a deepObject parameter has no nested object properties.\n"). -spec validate_deep_object_no_nested_objects( binary(), oaspec@openapi@spec:parameter(), oaspec@codegen@context:context() ) -> list(validation_error()). validate_deep_object_no_nested_objects(Path, Param, Ctx) -> case resolve_schema_object(erlang:element(6, Param), Ctx) of {some, {object_schema, _, Properties, _, _, _, _, _}} -> _pipe = maps:to_list(Properties), gleam@list:flat_map( _pipe, fun(Entry) -> {Prop_name, Prop_ref} = Entry, case resolve_schema_object({some, Prop_ref}, Ctx) of {some, {object_schema, _, _, _, _, _, _, _}} -> [{validation_error, <<<>/binary, Prop_name/binary>>, <<"Nested object properties in deepObject parameters are not supported. Only one level of object nesting is supported (e.g., filter[name]=value)."/utf8>>, severity_error, target_both}]; {some, {all_of_schema, _, _}} -> [{validation_error, <<<>/binary, Prop_name/binary>>, <<"Nested object properties in deepObject parameters are not supported. Only one level of object nesting is supported (e.g., filter[name]=value)."/utf8>>, severity_error, target_both}]; {some, {one_of_schema, _, _, _}} -> [{validation_error, <<<>/binary, Prop_name/binary>>, <<"Nested object properties in deepObject parameters are not supported. Only one level of object nesting is supported (e.g., filter[name]=value)."/utf8>>, severity_error, target_both}]; {some, {any_of_schema, _, _, _}} -> [{validation_error, <<<>/binary, Prop_name/binary>>, <<"Nested object properties in deepObject parameters are not supported. Only one level of object nesting is supported (e.g., filter[name]=value)."/utf8>>, severity_error, target_both}]; _ -> [] end end ); _ -> [] end. -file("src/oaspec/codegen/validate.gleam", 178). ?DOC( " Check if a parameter has a complex schema (object, oneOf, allOf, anyOf)\n" " that is not handled by deepObject style.\n" ). -spec validate_complex_param_schema( binary(), oaspec@openapi@spec:parameter(), oaspec@codegen@context:context() ) -> list(validation_error()). validate_complex_param_schema(Path, Param, Ctx) -> case erlang:element(7, Param) of {some, deep_object_style} -> validate_deep_object_no_nested_objects(Path, Param, Ctx); _ -> case erlang:element(3, Param) of in_path -> []; _ -> case resolve_schema_object(erlang:element(6, Param), Ctx) of {some, {object_schema, _, _, _, _, _, _, _}} -> [{validation_error, Path, <<"Complex schema (object/oneOf/allOf/anyOf) parameters require style: deepObject. Without it, the parameter cannot be serialized."/utf8>>, severity_error, target_both}]; {some, {all_of_schema, _, _}} -> [{validation_error, Path, <<"Complex schema (object/oneOf/allOf/anyOf) parameters require style: deepObject. Without it, the parameter cannot be serialized."/utf8>>, severity_error, target_both}]; {some, {one_of_schema, _, _, _}} -> [{validation_error, Path, <<"Complex schema (object/oneOf/allOf/anyOf) parameters require style: deepObject. Without it, the parameter cannot be serialized."/utf8>>, severity_error, target_both}]; {some, {any_of_schema, _, _, _}} -> [{validation_error, Path, <<"Complex schema (object/oneOf/allOf/anyOf) parameters require style: deepObject. Without it, the parameter cannot be serialized."/utf8>>, severity_error, target_both}]; _ -> [] end end end. -file("src/oaspec/codegen/validate.gleam", 134). ?DOC( " Validate parameters for unsupported serialization styles.\n" " Supported: form (default), deepObject (query+object), exploded array.\n" " Unsupported: matrix, label, simple, spaceDelimited, pipeDelimited.\n" ). -spec validate_parameters( binary(), list(oaspec@openapi@spec:parameter()), oaspec@codegen@context:context() ) -> list(validation_error()). validate_parameters(Op_id, Params, Ctx) -> gleam@list:flat_map( Params, fun(P) -> Path = <<<>/binary, (erlang:element(2, P))/binary>>, Style_errors = case erlang:element(7, P) of {some, matrix_style} -> [{validation_error, Path, <<"Parameter style is not supported. Supported styles: form, deepObject, simple."/utf8>>, severity_error, target_both}]; {some, label_style} -> [{validation_error, Path, <<"Parameter style is not supported. Supported styles: form, deepObject, simple."/utf8>>, severity_error, target_both}]; {some, space_delimited_style} -> [{validation_error, Path, <<"Parameter style is not supported. Supported styles: form, deepObject, simple."/utf8>>, severity_error, target_both}]; {some, pipe_delimited_style} -> [{validation_error, Path, <<"Parameter style is not supported. Supported styles: form, deepObject, simple."/utf8>>, severity_error, target_both}]; _ -> [] end, Content_errors = case erlang:element(6, P) of none -> [{validation_error, Path, <<"Parameters using 'content' instead of 'schema' are not supported."/utf8>>, severity_error, target_both}]; _ -> [] end, Complex_schema_errors = validate_complex_param_schema(Path, P, Ctx), lists:append([Style_errors, Content_errors, Complex_schema_errors]) end ). -file("src/oaspec/codegen/validate.gleam", 352). ?DOC( " Validate that application/x-www-form-urlencoded uses an object schema.\n" " Non-object schemas produce empty form bodies in the generated code.\n" ). -spec validate_form_urlencoded_schema( binary(), gleam@dict:dict(binary(), oaspec@openapi@spec:media_type()), oaspec@codegen@context:context() ) -> list(validation_error()). validate_form_urlencoded_schema(Op_id, Content, Ctx) -> case gleam_stdlib:map_get( Content, <<"application/x-www-form-urlencoded"/utf8>> ) of {ok, Media_type} -> case resolve_schema_object(erlang:element(2, Media_type), Ctx) of {some, {object_schema, _, _, _, _, _, _, _}} -> []; {some, _} -> [{validation_error, <>, <<"application/x-www-form-urlencoded request bodies must use an object schema."/utf8>>, severity_error, target_both}]; none -> [] end; {error, _} -> [] end. -file("src/oaspec/codegen/validate.gleam", 375). -spec multipart_field_is_stringifiable( oaspec@openapi@schema:schema_ref(), oaspec@codegen@context:context() ) -> boolean(). multipart_field_is_stringifiable(Schema_ref, Ctx) -> case resolve_schema_object({some, Schema_ref}, Ctx) of {some, {string_schema, _, _, _, _, _, _}} -> true; {some, {integer_schema, _, _, _, _, _, _, _}} -> true; {some, {number_schema, _, _, _, _, _, _, _}} -> true; {some, {boolean_schema, _}} -> true; _ -> false end. -file("src/oaspec/codegen/validate.gleam", 312). -spec validate_multipart_request_body_fields( binary(), gleam@dict:dict(binary(), oaspec@openapi@spec:media_type()), oaspec@codegen@context:context() ) -> list(validation_error()). validate_multipart_request_body_fields(Op_id, Content, Ctx) -> case gleam_stdlib:map_get(Content, <<"multipart/form-data"/utf8>>) of {ok, Media_type} -> case resolve_schema_object(erlang:element(2, Media_type), Ctx) of {some, {object_schema, _, Properties, _, _, _, _, _}} -> _pipe = maps:to_list(Properties), gleam@list:flat_map( _pipe, fun(Entry) -> {Field_name, Field_schema} = Entry, case multipart_field_is_stringifiable( Field_schema, Ctx ) of true -> []; false -> [{validation_error, <<<>/binary, Field_name/binary>>, <<"multipart/form-data fields must be string, integer, number, boolean, binary, or string enums."/utf8>>, severity_error, target_both}] end end ); {some, _} -> [{validation_error, <>, <<"multipart/form-data request bodies must use an object schema."/utf8>>, severity_error, target_both}]; none -> [] end; {error, _} -> [] end. -file("src/oaspec/codegen/validate.gleam", 546). ?DOC( " Validate security schemes for unsupported types.\n" " All scheme types are now supported: apiKey, HTTP (any scheme), OAuth2,\n" " and OpenID Connect.\n" ). -spec validate_security_schemes(oaspec@codegen@context:context()) -> list(validation_error()). validate_security_schemes(_) -> []. -file("src/oaspec/codegen/validate.gleam", 553). ?DOC( " Check for AST fields that are parsed but not used by codegen, emitting\n" " warnings so users are aware their spec contains features we preserve but\n" " do not generate code for.\n" ). -spec validate_preserved_but_unused(oaspec@codegen@context:context()) -> list(validation_error()). validate_preserved_but_unused(Ctx) -> Webhook_warnings = case gleam@dict:is_empty( erlang:element(8, erlang:element(2, Ctx)) ) of true -> []; false -> [{validation_error, <<"webhooks"/utf8>>, <<"Webhooks are parsed but not used by code generation."/utf8>>, severity_warning, target_both}] end, Operations = oaspec@codegen@types:collect_operations(Ctx), Response_warnings = gleam@list:flat_map( Operations, fun(Op) -> {Op_id, Operation, _, _} = Op, Entries = maps:to_list(erlang:element(8, Operation)), gleam@list:flat_map( Entries, fun(Entry) -> {Status_code, Response} = Entry, Base_path = <<<>/binary, Status_code/binary>>, Header_warnings = case gleam@dict:is_empty( erlang:element(4, Response) ) of true -> []; false -> [{validation_error, <>, <<"Response headers are parsed but not used by code generation."/utf8>>, severity_warning, target_both}] end, Link_warnings = case gleam@dict:is_empty( erlang:element(5, Response) ) of true -> []; false -> [{validation_error, <>, <<"Response links are parsed but not used by code generation."/utf8>>, severity_warning, target_both}] end, Content_entries = maps:to_list(erlang:element(3, Response)), Encoding_warnings = gleam@list:flat_map( Content_entries, fun(Ce) -> {Media_type_name, Media_type} = Ce, case gleam@dict:is_empty( erlang:element(5, Media_type) ) of true -> []; false -> [{validation_error, <<<<<>/binary, Media_type_name/binary>>/binary, ".encoding"/utf8>>, <<"MediaType encoding is parsed but not used by code generation."/utf8>>, severity_warning, target_both}] end end ), lists:append( [Header_warnings, Link_warnings, Encoding_warnings] ) end ) end ), lists:append([Webhook_warnings, Response_warnings]). -file("src/oaspec/codegen/validate.gleam", 481). ?DOC(" Recursively validate a SchemaObject, descending into all sub-schemas.\n"). -spec validate_schema_recursive( binary(), oaspec@openapi@schema:schema_object(), oaspec@codegen@context:context() ) -> list(validation_error()). validate_schema_recursive(Path, Schema_obj, Ctx) -> case Schema_obj of {object_schema, _, Properties, _, Additional_properties, Additional_properties_untyped, _, _} -> Ap_errors = [], _ = Additional_properties_untyped, Typed_ap_errors = case Additional_properties of {some, Ap_ref} -> validate_schema_ref_recursive( <>, Ap_ref, Ctx ); none -> [] end, Prop_errors = begin _pipe = maps:to_list(Properties), gleam@list:flat_map( _pipe, fun(Entry) -> {Prop_name, Prop_ref} = Entry, Prop_path = <<<>/binary, Prop_name/binary>>, validate_schema_ref_recursive(Prop_path, Prop_ref, Ctx) end ) end, lists:append([Ap_errors, Typed_ap_errors, Prop_errors]); {array_schema, _, Items, _, _, _} -> validate_schema_ref_recursive( <>, Items, Ctx ); {one_of_schema, _, Schemas, _} -> gleam@list:flat_map( Schemas, fun(S_ref) -> validate_schema_ref_recursive( <>, S_ref, Ctx ) end ); {any_of_schema, _, Schemas@1, _} -> gleam@list:flat_map( Schemas@1, fun(S_ref@1) -> validate_schema_ref_recursive( <>, S_ref@1, Ctx ) end ); {all_of_schema, _, Schemas@2} -> gleam@list:flat_map( Schemas@2, fun(S_ref@2) -> validate_schema_ref_recursive( <>, S_ref@2, Ctx ) end ); _ -> [] end. -file("src/oaspec/codegen/validate.gleam", 442). ?DOC( " Recursively validate a SchemaRef at any depth.\n" " References are checked for resolvability against the spec's components.\n" ). -spec validate_schema_ref_recursive( binary(), oaspec@openapi@schema:schema_ref(), oaspec@codegen@context:context() ) -> list(validation_error()). validate_schema_ref_recursive(Path, Schema_ref, Ctx) -> case Schema_ref of {reference, Ref, _} -> case gleam_stdlib:string_starts_with(Ref, <<"#/"/utf8>>) of false -> [{validation_error, Path, <<<<"External $ref '"/utf8, Ref/binary>>/binary, "' is not supported. Only local references (#/components/...) are supported."/utf8>>, severity_error, target_both}]; true -> case oaspec@openapi@resolver:resolve_schema_ref( Schema_ref, erlang:element(2, Ctx) ) of {ok, _} -> []; {error, _} -> [{validation_error, Path, <<<<"Unresolved schema reference: '"/utf8, Ref/binary>>/binary, "'. The referenced schema does not exist in components."/utf8>>, severity_error, target_both}] end end; {inline, Schema_obj} -> validate_schema_recursive(Path, Schema_obj, Ctx) end. -file("src/oaspec/codegen/validate.gleam", 257). ?DOC(" Validate request body for unsupported patterns.\n"). -spec validate_request_body( binary(), gleam@option:option(oaspec@openapi@spec:request_body()), oaspec@codegen@context:context() ) -> list(validation_error()). validate_request_body(Op_id, Request_body, Ctx) -> case Request_body of none -> []; {some, Rb} -> Content_keys = maps:keys(erlang:element(3, Rb)), Unsupported = gleam@list:filter( Content_keys, fun(Key) -> not oaspec@util@content_type:is_supported_request( oaspec@util@content_type:from_string(Key) ) end ), Content_type_errors = case Unsupported of [] -> []; [Media_type | _] -> [{validation_error, <>, <<<<"Content type '"/utf8, Media_type/binary>>/binary, "' is not supported. Supported request content types: application/json, multipart/form-data, application/x-www-form-urlencoded."/utf8>>, severity_error, target_both}] end, Schema_errors = begin _pipe = maps:to_list(erlang:element(3, Rb)), gleam@list:flat_map( _pipe, fun(Entry) -> {_, Media_type@1} = Entry, case erlang:element(2, Media_type@1) of {some, Schema_ref} -> validate_schema_ref_recursive( <>, Schema_ref, Ctx ); none -> [] end end ) end, Multipart_field_errors = validate_multipart_request_body_fields( Op_id, erlang:element(3, Rb), Ctx ), Form_urlencoded_errors = validate_form_urlencoded_schema( Op_id, erlang:element(3, Rb), Ctx ), lists:append( [Content_type_errors, Schema_errors, Multipart_field_errors, Form_urlencoded_errors] ) end. -file("src/oaspec/codegen/validate.gleam", 386). ?DOC(" Validate response schemas and content types.\n"). -spec validate_responses( binary(), gleam@dict:dict(binary(), oaspec@openapi@spec:response()), oaspec@codegen@context:context() ) -> list(validation_error()). validate_responses(Op_id, Responses, Ctx) -> Entries = maps:to_list(Responses), gleam@list:flat_map( Entries, fun(Entry) -> {Status_code, Response} = Entry, Content_entries = maps:to_list(erlang:element(3, Response)), gleam@list:flat_map( Content_entries, fun(Ce) -> {Media_type_name, Media_type} = Ce, Path = <<<>/binary, Status_code/binary>>, Content_type_errors = case oaspec@util@content_type:is_supported_response( oaspec@util@content_type:from_string(Media_type_name) ) of true -> []; false -> [{validation_error, Path, <<<<"Response content type '"/utf8, Media_type_name/binary>>/binary, "' is not supported. Supported response content types: application/json, text/plain, application/octet-stream, application/xml, text/xml."/utf8>>, severity_error, target_both}] end, Schema_errors = case erlang:element(2, Media_type) of {some, Schema_ref} -> validate_schema_ref_recursive(Path, Schema_ref, Ctx); none -> [] end, lists:append(Content_type_errors, Schema_errors) end ) end ). -file("src/oaspec/codegen/validate.gleam", 72). ?DOC(" Validate all operations for unsupported patterns.\n"). -spec validate_operations(oaspec@codegen@context:context()) -> list(validation_error()). validate_operations(Ctx) -> Operations = oaspec@codegen@types:collect_operations(Ctx), gleam@list:flat_map( Operations, fun(Op) -> {Op_id, Operation, Path, _} = Op, Path_errors = validate_path_template_params( Op_id, Path, erlang:element(6, Operation) ), Param_errors = validate_parameters( Op_id, erlang:element(6, Operation), Ctx ), Body_errors = validate_request_body( Op_id, erlang:element(7, Operation), Ctx ), Response_errors = validate_responses( Op_id, erlang:element(8, Operation), Ctx ), lists:append( [Path_errors, Param_errors, Body_errors, Response_errors] ) end ). -file("src/oaspec/codegen/validate.gleam", 425). ?DOC(" Validate component schemas recursively.\n"). -spec validate_component_schemas(oaspec@codegen@context:context()) -> list(validation_error()). validate_component_schemas(Ctx) -> Schemas = case erlang:element(5, erlang:element(2, Ctx)) of {some, Components} -> maps:to_list(erlang:element(2, Components)); none -> [] end, gleam@list:flat_map( Schemas, fun(Entry) -> {Name, Schema_ref} = Entry, validate_schema_ref_recursive( <<"components.schemas."/utf8, Name/binary>>, Schema_ref, Ctx ) end ). -file("src/oaspec/codegen/validate.gleam", 44). ?DOC( " Validate the parsed spec for unsupported patterns.\n" " Returns a list of errors; empty list means validation passed.\n" " Name collisions and duplicate operationIds are handled by the dedup pass\n" " before validation, so they are no longer checked here.\n" ). -spec validate(oaspec@codegen@context:context()) -> list(validation_error()). validate(Ctx) -> Op_errors = validate_operations(Ctx), Schema_errors = validate_component_schemas(Ctx), Security_errors = validate_security_schemes(Ctx), Preserved_warnings = validate_preserved_but_unused(Ctx), lists:append( [Op_errors, Schema_errors, Security_errors, Preserved_warnings] ).