Packages
oaspec
0.49.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.gleam
import gleam/list
import gleam/result
import oaspec/config.{type Config, Both, Client, Server}
import oaspec/internal/codegen/client
import oaspec/internal/codegen/context.{type Context, type GeneratedFile}
import oaspec/internal/codegen/decoders
import oaspec/internal/codegen/encoders
import oaspec/internal/codegen/guards
import oaspec/internal/codegen/server
import oaspec/internal/codegen/types
import oaspec/internal/codegen/validate
import oaspec/internal/openapi/capability_check
import oaspec/internal/openapi/dedup
import oaspec/internal/openapi/filter
import oaspec/internal/openapi/hoist
import oaspec/internal/openapi/location_index.{type LocationIndex}
import oaspec/internal/openapi/normalize
import oaspec/internal/openapi/resolve
import oaspec/internal/openapi/spec.{type OpenApiSpec, type Unresolved}
import oaspec/internal/progress.{type Reporter}
import oaspec/openapi/diagnostic.{type Diagnostic}
/// Result of a successful code generation run.
pub type GenerationSummary {
GenerationSummary(
files: List(GeneratedFile),
spec_title: String,
warnings: List(Diagnostic),
)
}
/// Result of a successful validation-only run.
pub type ValidationSummary {
ValidationSummary(spec_title: String, warnings: List(Diagnostic))
}
/// Errors from the pure generation pipeline.
pub type GenerateError {
ValidationErrors(errors: List(Diagnostic))
}
/// Intermediate result from the shared pipeline.
/// Contains the validated context and accumulated warnings.
type PreparedContext {
PreparedContext(ctx: Context, spec_title: String, warnings: List(Diagnostic))
}
/// Shared pipeline: normalize → resolve → capability_check → hoist → dedup → validate.
/// Returns a validated context with accumulated warnings, or errors.
///
/// Each stage is wrapped with `progress.timed` so `reporter` sees a
/// "stage: X took Yms" line per phase. The GitHub REST OpenAPI is
/// large enough that without these lines callers can't tell whether
/// the process is hung or working — see issue #352.
fn prepare_context(
spec: OpenApiSpec(Unresolved),
cfg: Config,
reporter: Reporter,
index: LocationIndex,
) -> Result(PreparedContext, GenerateError) {
let spec_title = spec.info.title <> " v" <> spec.info.version
// Normalize OAS 3.1 patterns to 3.0-compatible form
let #(elapsed, spec) = progress.timed(fn() { normalize.normalize(spec) })
progress.report(
reporter,
"normalize OAS 3.1 → 3.0 patterns (took "
<> progress.format_ms(elapsed)
<> ")",
)
// Resolve component entry aliases ($ref within components)
let #(elapsed, resolved) = progress.timed(fn() { resolve.resolve(spec) })
progress.report(
reporter,
"resolve component $ref aliases (took "
<> progress.format_ms(elapsed)
<> ")",
)
use spec <- result.try(
resolved
|> result.map_error(fn(errors) { ValidationErrors(errors:) }),
)
// Issue #387: apply the include filter (if any) before capability
// check / hoist / validate so every downstream stage sees only the
// operations the user asked for. Empty filter is a no-op.
let #(elapsed, spec) =
progress.timed(fn() { filter.apply(spec, config.include(cfg)) })
progress.report(
reporter,
"apply include filter (took " <> progress.format_ms(elapsed) <> ")",
)
// Check for unsupported features using capability registry
let #(elapsed, capability_issues) =
progress.timed(fn() {
capability_check.check(spec, index)
|> diagnostic.filter_by_mode(config.mode(cfg))
})
progress.report(
reporter,
"capability check (took " <> progress.format_ms(elapsed) <> ")",
)
let capability_errors = diagnostic.errors_only(capability_issues)
let capability_warnings = diagnostic.warnings_only(capability_issues)
use _ <- result.try(case list.is_empty(capability_errors) {
False -> Error(ValidationErrors(errors: capability_errors))
True -> Ok(Nil)
})
// Hoist inline complex schemas into components.schemas
let #(elapsed, spec) = progress.timed(fn() { hoist.hoist(spec) })
progress.report(
reporter,
"hoist inline complex schemas (took " <> progress.format_ms(elapsed) <> ")",
)
// Deduplicate names to avoid collisions in generated code
let #(elapsed, spec) = progress.timed(fn() { dedup.dedup(spec) })
progress.report(
reporter,
"deduplicate generated names (took " <> progress.format_ms(elapsed) <> ")",
)
// Create generation context
let ctx = context.new(spec, cfg)
// Check for parsed-but-unused features (capability warnings)
let #(elapsed, preserved_warnings) =
progress.timed(fn() {
capability_check.check_preserved(ctx, index)
|> diagnostic.filter_by_mode(config.mode(cfg))
})
progress.report(
reporter,
"preserved-feature warnings (took " <> progress.format_ms(elapsed) <> ")",
)
// Validate spec for unsupported features
let #(elapsed, validation_issues) =
progress.timed(fn() {
validate.validate(ctx)
|> diagnostic.filter_by_mode(config.mode(cfg))
})
progress.report(
reporter,
"validate spec for unsupported features (took "
<> progress.format_ms(elapsed)
<> ")",
)
let blocking_errors = diagnostic.errors_only(validation_issues)
let validation_warnings = diagnostic.warnings_only(validation_issues)
use _ <- result.try(case list.is_empty(blocking_errors) {
False -> Error(ValidationErrors(errors: blocking_errors))
True -> Ok(Nil)
})
let warnings =
list.flatten([capability_warnings, preserved_warnings, validation_warnings])
Ok(PreparedContext(ctx:, spec_title:, warnings:))
}
/// Pure generation pipeline: parse → normalize → resolve → capability_check → hoist → dedup → validate → codegen.
/// Takes an already-parsed spec and config; returns generated files or errors.
/// Does not perform IO — callers handle writing files and printing output.
///
/// Capability-check diagnostics from this entry point carry no source
/// location information. Callers that already have a YAML
/// `LocationIndex` (e.g. via `parser.parse_file_with_locations`)
/// should prefer `generate_with_locations` so capability-check errors
/// surface line/column for the offending spec node (Issue #411).
pub fn generate(
spec: OpenApiSpec(Unresolved),
cfg: Config,
) -> Result(GenerationSummary, GenerateError) {
generate_with_progress_and_locations(
spec,
location_index.empty(),
cfg,
progress.noop(),
)
}
/// Combined entry point that accepts both a `LocationIndex` and a
/// `Reporter`. Issue #411 + #352. The CLI uses this so it can show
/// per-stage progress AND surface `path:line:column:` in capability
/// errors at the same time. Library callers that need only one of the
/// two can pass `location_index.empty()` or `progress.noop()`.
pub fn generate_with_progress_and_locations(
spec: OpenApiSpec(Unresolved),
index: LocationIndex,
cfg: Config,
reporter: Reporter,
) -> Result(GenerationSummary, GenerateError) {
use prepared <- result.try(prepare_context(spec, cfg, reporter, index))
let #(elapsed, files) =
progress.timed(fn() { generate_all_files(prepared.ctx) })
progress.report(
reporter,
"render generated source files (took " <> progress.format_ms(elapsed) <> ")",
)
Ok(GenerationSummary(
files:,
spec_title: prepared.spec_title,
warnings: prepared.warnings,
))
}
/// Validation-only pipeline: parse → normalize → resolve → capability_check → hoist → dedup → validate.
/// Runs the same checks as generate() but skips code generation and file writing.
pub fn validate_only(
spec: OpenApiSpec(Unresolved),
cfg: Config,
) -> Result(ValidationSummary, GenerateError) {
validate_only_with_progress_and_locations(
spec,
location_index.empty(),
cfg,
progress.noop(),
)
}
/// Combined `validate_only` entry point that accepts both a
/// `LocationIndex` and a `Reporter`. Issue #411 + #352.
pub fn validate_only_with_progress_and_locations(
spec: OpenApiSpec(Unresolved),
index: LocationIndex,
cfg: Config,
reporter: Reporter,
) -> Result(ValidationSummary, GenerateError) {
use prepared <- result.try(prepare_context(spec, cfg, reporter, index))
Ok(ValidationSummary(
spec_title: prepared.spec_title,
warnings: prepared.warnings,
))
}
/// Pure file generation: produce all GeneratedFile values without any IO.
pub fn generate_all_files(ctx: Context) -> List(GeneratedFile) {
let shared = generate_shared(ctx)
let server_files = case config.mode(context.config(ctx)) {
Server | Both -> server.generate(ctx)
Client -> []
}
let client_files = case config.mode(context.config(ctx)) {
Client | Both -> client.generate(ctx)
Server -> []
}
list.flatten([shared, server_files, client_files])
}
/// Generate shared files (types, decoders, encoders, guards).
///
/// `middleware.gleam` used to be emitted here too, but its `Handler` shape
/// did not actually compose with the generated client or server APIs (see
/// issue #116). It is no longer part of the default generated surface;
/// the `oaspec/internal/codegen/middleware` module is kept only as a library-level
/// helper for consumers who want to assemble their own middleware chain.
fn generate_shared(ctx: Context) -> List(GeneratedFile) {
let type_files = types.generate(ctx)
let decoder_files = decoders.generate(ctx)
let encoder_files = encoders.generate(ctx)
let guard_files = guards.generate(ctx)
list.flatten([type_files, decoder_files, encoder_files, guard_files])
}