Current section

Files

Jump to
oaspec src oaspec@generate.erl
Raw

src/oaspec@generate.erl

-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 `<phase> (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()).