Packages
oaspec
0.62.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.1
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.3
0.6.1
0.6.0
0.5.0
0.4.0
0.3.0
0.1.3
Generate Gleam code from OpenAPI 3.x specifications
Current section
Files
Jump to
Current section
Files
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@internal@openapi@spec:open_api_spec(oaspec@internal@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@internal@openapi@spec:open_api_spec(oaspec@internal@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@internal@openapi@spec:open_api_spec(oaspec@internal@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@internal@openapi@spec:open_api_spec(oaspec@internal@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@internal@openapi@spec:open_api_spec(oaspec@internal@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()).