Current section

Files

Jump to
oaspec src oaspec generate.gleam
Raw

src/oaspec/generate.gleam

import gleam/list
import gleam/result
import oaspec/codegen/client
import oaspec/codegen/context.{type Context, type GeneratedFile}
import oaspec/codegen/decoders
import oaspec/codegen/encoders
import oaspec/codegen/guards
import oaspec/codegen/server
import oaspec/codegen/types
import oaspec/codegen/validate
import oaspec/config.{type Config, Both, Client, Server}
import oaspec/openapi/capability_check
import oaspec/openapi/dedup
import oaspec/openapi/diagnostic.{type Diagnostic}
import oaspec/openapi/hoist
import oaspec/openapi/normalize
import oaspec/openapi/resolve
import oaspec/openapi/spec.{type OpenApiSpec, type Unresolved}
/// 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.
fn prepare_context(
spec: OpenApiSpec(Unresolved),
cfg: Config,
) -> Result(PreparedContext, GenerateError) {
let spec_title = spec.info.title <> " v" <> spec.info.version
// Normalize OAS 3.1 patterns to 3.0-compatible form
let spec = normalize.normalize(spec)
// Resolve component entry aliases ($ref within components)
use spec <- result.try(
resolve.resolve(spec)
|> result.map_error(fn(errors) { ValidationErrors(errors:) }),
)
// Check for unsupported features using capability registry
let capability_issues =
capability_check.check(spec)
|> validate.filter_by_mode(config.mode(cfg))
let capability_errors = validate.errors_only(capability_issues)
let capability_warnings = validate.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 spec = hoist.hoist(spec)
// Deduplicate names to avoid collisions in generated code
let spec = dedup.dedup(spec)
// Create generation context
let ctx = context.new(spec, cfg)
// Check for parsed-but-unused features (capability warnings)
let preserved_warnings =
capability_check.check_preserved(ctx)
|> diagnostic.filter_by_mode(config.mode(cfg))
// Validate spec for unsupported features
let validation_issues =
validate.validate(ctx)
|> validate.filter_by_mode(config.mode(cfg))
let blocking_errors = validate.errors_only(validation_issues)
let validation_warnings = validate.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.
pub fn generate(
spec: OpenApiSpec(Unresolved),
cfg: Config,
) -> Result(GenerationSummary, GenerateError) {
use prepared <- result.try(prepare_context(spec, cfg))
let files = generate_all_files(prepared.ctx)
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) {
use prepared <- result.try(prepare_context(spec, cfg))
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/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])
}