-module(oaspec@generate). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/oaspec/generate.gleam"). -export([validate_only_with_progress_and_locations/4, validate_only/2, generate_with_progress_and_locations/4, generate/2, generate_all_files/1]). -export_type([generation_summary/0, validation_summary/0, generate_error/0, prepared_context/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 generation_summary() :: {generation_summary, list(oaspec@internal@codegen@context:generated_file()), binary(), list(oaspec@openapi@diagnostic:diagnostic())}. -type validation_summary() :: {validation_summary, binary(), list(oaspec@openapi@diagnostic:diagnostic())}. -type generate_error() :: {validation_errors, list(oaspec@openapi@diagnostic:diagnostic())}. -type prepared_context() :: {prepared_context, oaspec@internal@codegen@context:context(), binary(), list(oaspec@openapi@diagnostic:diagnostic())}. -file("src/oaspec/generate.gleam", 56). ?DOC( " Shared pipeline: normalize → resolve → capability_check → hoist → dedup → validate.\n" " Returns a validated context with accumulated warnings, or errors.\n" "\n" " Each stage is wrapped with `progress.timed` so `reporter` sees a\n" " \"stage: X took Yms\" line per phase. The GitHub REST OpenAPI is\n" " large enough that without these lines callers can't tell whether\n" " the process is hung or working — see issue #352.\n" ). -spec prepare_context( oaspec@openapi@spec:open_api_spec(oaspec@openapi@spec:unresolved()), oaspec@config:config(), oaspec@internal@progress:reporter(), oaspec@internal@openapi@location_index:location_index() ) -> {ok, prepared_context()} | {error, generate_error()}. prepare_context(Spec, Cfg, Reporter, Index) -> Spec_title = <<<<(erlang:element(2, erlang:element(3, Spec)))/binary, " v"/utf8>>/binary, (erlang:element(4, erlang:element(3, Spec)))/binary>>, {Elapsed, Spec@1} = oaspec@internal@progress:timed( fun() -> oaspec@internal@openapi@normalize:normalize(Spec) end ), oaspec@internal@progress:report( Reporter, <<<<"normalize OAS 3.1 → 3.0 patterns (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed))/binary>>/binary, ")"/utf8>> ), {Elapsed@1, Resolved} = oaspec@internal@progress:timed( fun() -> oaspec@internal@openapi@resolve:resolve(Spec@1) end ), oaspec@internal@progress:report( Reporter, <<<<"resolve component $ref aliases (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@1))/binary>>/binary, ")"/utf8>> ), gleam@result:'try'( begin _pipe = Resolved, gleam@result:map_error( _pipe, fun(Errors) -> {validation_errors, Errors} end ) end, fun(Spec@2) -> {Elapsed@2, Spec@3} = oaspec@internal@progress:timed( fun() -> oaspec@internal@openapi@filter:apply( Spec@2, oaspec@config:include(Cfg) ) end ), oaspec@internal@progress:report( Reporter, <<<<"apply include filter (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@2))/binary>>/binary, ")"/utf8>> ), {Elapsed@3, Capability_issues} = oaspec@internal@progress:timed( fun() -> _pipe@1 = oaspec@internal@openapi@capability_check:check( Spec@3, Index ), oaspec@openapi@diagnostic:filter_by_mode( _pipe@1, oaspec@config:mode(Cfg) ) end ), oaspec@internal@progress:report( Reporter, <<<<"capability check (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@3))/binary>>/binary, ")"/utf8>> ), Capability_errors = oaspec@openapi@diagnostic:errors_only( Capability_issues ), Capability_warnings = oaspec@openapi@diagnostic:warnings_only( Capability_issues ), gleam@result:'try'(case gleam@list:is_empty(Capability_errors) of false -> {error, {validation_errors, Capability_errors}}; true -> {ok, nil} end, fun(_) -> {Elapsed@4, Spec@4} = oaspec@internal@progress:timed( fun() -> oaspec@internal@openapi@hoist:hoist(Spec@3) end ), oaspec@internal@progress:report( Reporter, <<<<"hoist inline complex schemas (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@4))/binary>>/binary, ")"/utf8>> ), Include_active = not oaspec@config:include_is_empty( oaspec@config:include(Cfg) ), {Elapsed@5, Spec@5} = oaspec@internal@progress:timed( fun() -> case Include_active of true -> oaspec@internal@openapi@reachability:prune( Spec@4 ); false -> Spec@4 end end ), oaspec@internal@progress:report( Reporter, <<<<"prune unreachable component schemas (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@5))/binary>>/binary, ")"/utf8>> ), {Elapsed@6, Spec@6} = oaspec@internal@progress:timed( fun() -> oaspec@internal@openapi@dedup:dedup(Spec@5) end ), oaspec@internal@progress:report( Reporter, <<<<"deduplicate generated names (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@6))/binary>>/binary, ")"/utf8>> ), Ctx = oaspec@internal@codegen@context:new(Spec@6, Cfg), {Elapsed@7, Preserved_warnings} = oaspec@internal@progress:timed( fun() -> _pipe@2 = oaspec@internal@openapi@capability_check:check_preserved( Ctx, Index ), oaspec@openapi@diagnostic:filter_by_mode( _pipe@2, oaspec@config:mode(Cfg) ) end ), oaspec@internal@progress:report( Reporter, <<<<"preserved-feature warnings (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@7))/binary>>/binary, ")"/utf8>> ), {Elapsed@8, Validation_issues} = oaspec@internal@progress:timed( fun() -> _pipe@3 = oaspec@internal@codegen@validate:validate( Ctx ), oaspec@openapi@diagnostic:filter_by_mode( _pipe@3, oaspec@config:mode(Cfg) ) end ), oaspec@internal@progress:report( Reporter, <<<<"validate spec for unsupported features (took "/utf8, (oaspec@internal@progress:format_ms(Elapsed@8))/binary>>/binary, ")"/utf8>> ), Blocking_errors = oaspec@openapi@diagnostic:errors_only( Validation_issues ), Validation_warnings = oaspec@openapi@diagnostic:warnings_only( Validation_issues ), gleam@result:'try'( case gleam@list:is_empty(Blocking_errors) of false -> {error, {validation_errors, Blocking_errors}}; true -> {ok, nil} end, fun(_) -> Warnings = lists:append( [Capability_warnings, Preserved_warnings, Validation_warnings] ), {ok, {prepared_context, Ctx, Spec_title, Warnings}} end ) end) end ). -file("src/oaspec/generate.gleam", 244). ?DOC( " Combined `validate_only` entry point that accepts both a\n" " `LocationIndex` and a `Reporter`. Issue #411 + #352.\n" ). -spec validate_only_with_progress_and_locations( oaspec@openapi@spec:open_api_spec(oaspec@openapi@spec:unresolved()), oaspec@internal@openapi@location_index:location_index(), oaspec@config:config(), oaspec@internal@progress:reporter() ) -> {ok, validation_summary()} | {error, generate_error()}. validate_only_with_progress_and_locations(Spec, Index, Cfg, Reporter) -> gleam@result:'try'( prepare_context(Spec, Cfg, Reporter, Index), fun(Prepared) -> {ok, {validation_summary, erlang:element(3, Prepared), erlang:element(4, Prepared)}} end ). -file("src/oaspec/generate.gleam", 230). ?DOC( " Validation-only pipeline: parse → normalize → resolve → capability_check → hoist → dedup → validate.\n" " Runs the same checks as generate() but skips code generation and file writing.\n" ). -spec validate_only( oaspec@openapi@spec:open_api_spec(oaspec@openapi@spec:unresolved()), oaspec@config:config() ) -> {ok, validation_summary()} | {error, generate_error()}. validate_only(Spec, Cfg) -> validate_only_with_progress_and_locations( Spec, oaspec@internal@openapi@location_index:empty(), Cfg, oaspec@internal@progress:noop() ). -file("src/oaspec/generate.gleam", 278). ?DOC( " Pure file generation with per-substage progress events.\n" "\n" " Each substage emits its own ` (took ...)` line via\n" " `progress.timed_stage` so a slow phase surfaces against a specific\n" " stage rather than the opaque outer render wrapper.\n" "\n" " `middleware.gleam` used to be emitted here too, but its `Handler`\n" " shape did not actually compose with the generated client or server\n" " APIs. The internal `middleware` module is kept only as a\n" " library-level helper for consumers who want to assemble their own\n" " middleware chain.\n" ). -spec generate_all_files_with_progress( oaspec@internal@codegen@context:context(), oaspec@internal@progress:reporter() ) -> list(oaspec@internal@codegen@context:generated_file()). generate_all_files_with_progress(Ctx, Reporter) -> Type_files = oaspec@internal@progress:timed_stage( Reporter, <<"generate types"/utf8>>, fun() -> oaspec@internal@codegen@types:generate(Ctx) end ), Decoder_files = oaspec@internal@progress:timed_stage( Reporter, <<"generate decoders"/utf8>>, fun() -> oaspec@internal@codegen@decoders:generate(Ctx) end ), Encoder_files = oaspec@internal@progress:timed_stage( Reporter, <<"generate encoders"/utf8>>, fun() -> oaspec@internal@codegen@encoders:generate(Ctx) end ), Guard_files = oaspec@internal@progress:timed_stage( Reporter, <<"generate guards"/utf8>>, fun() -> oaspec@internal@codegen@guards:generate(Ctx) end ), Server_files = case oaspec@config:mode( oaspec@internal@codegen@context:config(Ctx) ) of server -> oaspec@internal@progress:timed_stage( Reporter, <<"generate server"/utf8>>, fun() -> oaspec@internal@codegen@server:generate(Ctx) end ); both -> oaspec@internal@progress:timed_stage( Reporter, <<"generate server"/utf8>>, fun() -> oaspec@internal@codegen@server:generate(Ctx) end ); client -> [] end, Client_files = case oaspec@config:mode( oaspec@internal@codegen@context:config(Ctx) ) of client -> oaspec@internal@progress:timed_stage( Reporter, <<"generate client"/utf8>>, fun() -> oaspec@internal@codegen@client:generate(Ctx) end ); both -> oaspec@internal@progress:timed_stage( Reporter, <<"generate client"/utf8>>, fun() -> oaspec@internal@codegen@client:generate(Ctx) end ); server -> [] end, lists:append( [Type_files, Decoder_files, Encoder_files, Guard_files, Server_files, Client_files] ). -file("src/oaspec/generate.gleam", 213). ?DOC( " Combined entry point that accepts both a `LocationIndex` and a\n" " `Reporter`. Issue #411 + #352. The CLI uses this so it can show\n" " per-stage progress AND surface `path:line:column:` in capability\n" " errors at the same time. Library callers that need only one of the\n" " two can pass `location_index.empty()` or `progress.noop()`.\n" ). -spec generate_with_progress_and_locations( oaspec@openapi@spec:open_api_spec(oaspec@openapi@spec:unresolved()), oaspec@internal@openapi@location_index:location_index(), oaspec@config:config(), oaspec@internal@progress:reporter() ) -> {ok, generation_summary()} | {error, generate_error()}. generate_with_progress_and_locations(Spec, Index, Cfg, Reporter) -> gleam@result:'try'( prepare_context(Spec, Cfg, Reporter, Index), fun(Prepared) -> Files = generate_all_files_with_progress( erlang:element(2, Prepared), Reporter ), {ok, {generation_summary, Files, erlang:element(3, Prepared), erlang:element(4, Prepared)}} end ). -file("src/oaspec/generate.gleam", 196). ?DOC( " Pure generation pipeline: parse → normalize → resolve → capability_check → hoist → dedup → validate → codegen.\n" " Takes an already-parsed spec and config; returns generated files or errors.\n" " Does not perform IO — callers handle writing files and printing output.\n" "\n" " Capability-check diagnostics from this entry point carry no source\n" " location information. Callers that already have a YAML\n" " `LocationIndex` (e.g. via `parser.parse_file_with_locations`)\n" " should prefer `generate_with_locations` so capability-check errors\n" " surface line/column for the offending spec node (Issue #411).\n" ). -spec generate( oaspec@openapi@spec:open_api_spec(oaspec@openapi@spec:unresolved()), oaspec@config:config() ) -> {ok, generation_summary()} | {error, generate_error()}. generate(Spec, Cfg) -> generate_with_progress_and_locations( Spec, oaspec@internal@openapi@location_index:empty(), Cfg, oaspec@internal@progress:noop() ). -file("src/oaspec/generate.gleam", 263). ?DOC( " Pure file generation: produce all GeneratedFile values without any IO.\n" "\n" " The `Reporter` plumbing lives in `generate_all_files_with_progress`;\n" " callers that want progress events (the CLI) go through the\n" " progress-aware wrapper. Library callers that need the values\n" " without progress noise pay nothing — `progress.noop()` is cheap.\n" ). -spec generate_all_files(oaspec@internal@codegen@context:context()) -> list(oaspec@internal@codegen@context:generated_file()). generate_all_files(Ctx) -> generate_all_files_with_progress(Ctx, oaspec@internal@progress:noop()).