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/codegen/writer.gleam
import gleam/list
import gleam/result
import oaspec/config.{Both, Client, Server}
import oaspec/internal/codegen/context.{type GeneratedFile}
import simplifile
/// Errors that can occur during file writing.
pub type WriteError {
DirectoryCreateError(path: String, detail: String)
FileWriteError(path: String, detail: String)
}
/// Write pre-generated files to disk based on configuration.
pub fn write_all(
files: List(GeneratedFile),
cfg: config.Config,
on_write: fn(String) -> Nil,
) -> Result(List(String), WriteError) {
// Separate files by their target kind (ADT-based, not filename matching)
let shared_files =
list.filter(files, fn(f) { f.target == context.SharedTarget })
let server_files =
list.filter(files, fn(f) { f.target == context.ServerTarget })
let client_files =
list.filter(files, fn(f) { f.target == context.ClientTarget })
let server_path = config.output_server(cfg)
let client_path = config.output_client(cfg)
let written_files = []
use written_files <- result.try(case config.mode(cfg) {
Server | Both -> {
use _ <- result.try(ensure_directory(server_path))
write_files(shared_files, server_path, written_files, on_write)
|> result.try(fn(w) {
write_files(server_files, server_path, w, on_write)
})
}
Client -> Ok(written_files)
})
use written_files <- result.try(case config.mode(cfg) {
Client | Both -> {
use _ <- result.try(ensure_directory(client_path))
write_files(shared_files, client_path, written_files, on_write)
|> result.try(fn(w) {
write_files(client_files, client_path, w, on_write)
})
}
Server -> Ok(written_files)
})
Ok(written_files)
}
/// Ensure a directory exists, creating it if necessary.
fn ensure_directory(path: String) -> Result(Nil, WriteError) {
simplifile.create_directory_all(path)
|> result.map_error(fn(err) {
DirectoryCreateError(
path:,
detail: "Failed to create directory: " <> simplifile.describe_error(err),
)
})
}
/// Write a list of generated files to a directory.
///
/// Issue #247: files marked `SkipIfExists` (currently `handlers.gleam`)
/// are written only on first generation. If the file already exists on
/// disk, it is left alone — the user owns the contents and the
/// generator must not clobber their implementation.
fn write_files(
files: List(GeneratedFile),
base_path: String,
written: List(String),
on_write: fn(String) -> Nil,
) -> Result(List(String), WriteError) {
list.try_fold(files, written, fn(acc, file) {
let full_path = base_path <> "/" <> file.path
case file.write_mode, simplifile.is_file(full_path) {
context.SkipIfExists, Ok(True) -> {
// File already exists; leave it alone. Don't notify on_write —
// generation status messages should reflect what was actually
// written.
Ok(acc)
}
_, _ -> {
use _ <- result.try(
simplifile.write(full_path, file.content)
|> result.map_error(fn(err) {
FileWriteError(
path: full_path,
detail: "Failed to write file: " <> simplifile.describe_error(err),
)
}),
)
on_write(full_path)
Ok([full_path, ..acc])
}
}
})
}
/// Resolve generated files to their full output paths and content.
/// Used by --check to compare against existing files without writing.
///
/// Issue #247: `SkipIfExists` files (e.g. user-owned `handlers.gleam`)
/// are dropped from the result. `--check` is meant to flag drift in
/// generator-owned files; user-edited files are expected to differ
/// from the bootstrap stub and should not be reported as out-of-date.
pub fn resolve_paths(
files: List(GeneratedFile),
cfg: config.Config,
) -> List(#(String, String)) {
let files = list.filter(files, fn(f) { f.write_mode != context.SkipIfExists })
let shared_files =
list.filter(files, fn(f) { f.target == context.SharedTarget })
let server_files =
list.filter(files, fn(f) { f.target == context.ServerTarget })
let client_files =
list.filter(files, fn(f) { f.target == context.ClientTarget })
let server_path = config.output_server(cfg)
let client_path = config.output_client(cfg)
let server_entries = case config.mode(cfg) {
Server | Both ->
list.map(list.append(shared_files, server_files), fn(f) {
#(server_path <> "/" <> f.path, f.content)
})
Client -> []
}
let client_entries = case config.mode(cfg) {
Client | Both ->
list.map(list.append(shared_files, client_files), fn(f) {
#(client_path <> "/" <> f.path, f.content)
})
Server -> []
}
list.append(server_entries, client_entries)
}
/// Every file path the generator would write for the given config — including
/// `SkipIfExists` files. Used by `--check` to keep user-owned `handlers.gleam`
/// off the orphan list while still excluding it from byte-comparison drift
/// reports (`resolve_paths` drops `SkipIfExists` entries).
pub fn expected_paths(
files: List(GeneratedFile),
cfg: config.Config,
) -> List(String) {
let shared_files =
list.filter(files, fn(f) { f.target == context.SharedTarget })
let server_files =
list.filter(files, fn(f) { f.target == context.ServerTarget })
let client_files =
list.filter(files, fn(f) { f.target == context.ClientTarget })
let server_path = config.output_server(cfg)
let client_path = config.output_client(cfg)
let server_entries = case config.mode(cfg) {
Server | Both ->
list.map(list.append(shared_files, server_files), fn(f) {
server_path <> "/" <> f.path
})
Client -> []
}
let client_entries = case config.mode(cfg) {
Client | Both ->
list.map(list.append(shared_files, client_files), fn(f) {
client_path <> "/" <> f.path
})
Server -> []
}
list.append(server_entries, client_entries)
}
/// Return the output directories that would be written to for the given config.
pub fn output_dirs(cfg: config.Config) -> List(String) {
case config.mode(cfg) {
Server -> [config.output_server(cfg)]
Client -> [config.output_client(cfg)]
Both -> [config.output_server(cfg), config.output_client(cfg)]
}
}
/// Convert a write error to a human-readable string.
pub fn error_to_string(error: WriteError) -> String {
case error {
DirectoryCreateError(path:, detail:) ->
"Failed to create directory " <> path <> ": " <> detail
FileWriteError(path:, detail:) ->
"Failed to write file " <> path <> ": " <> detail
}
}