Packages
oaspec
0.4.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/server.gleam
import gleam/list
import gleam/option.{Some}
import oaspec/codegen/context.{type Context, type GeneratedFile, GeneratedFile}
import oaspec/codegen/types as type_gen
import oaspec/openapi/spec
import oaspec/util/naming
import oaspec/util/string_extra as se
/// Generate server stub files.
pub fn generate(ctx: Context) -> List(GeneratedFile) {
let handlers_content = generate_handlers(ctx)
let router_content = generate_router(ctx)
[
GeneratedFile(path: "handlers.gleam", content: handlers_content),
GeneratedFile(path: "router.gleam", content: router_content),
]
}
/// Generate handler stubs for all operations.
fn generate_handlers(ctx: Context) -> String {
let sb =
se.file_header(context.version)
|> se.imports([
ctx.config.package <> "/request_types",
ctx.config.package <> "/response_types",
])
let operations = type_gen.collect_operations(ctx)
let sb =
list.fold(operations, sb, fn(sb, op) {
let #(op_id, operation, _path, _method) = op
generate_handler(sb, op_id, operation, ctx)
})
se.to_string(sb)
}
/// Generate a single handler stub.
fn generate_handler(
sb: se.StringBuilder,
op_id: String,
operation: spec.Operation,
_ctx: Context,
) -> se.StringBuilder {
let fn_name = naming.operation_to_function_name(op_id)
let request_type = naming.schema_to_type_name(op_id) <> "Request"
let response_type = naming.schema_to_type_name(op_id) <> "Response"
let sb = case operation.summary {
Some(summary) -> sb |> se.doc_comment(summary)
_ -> sb
}
let sb = case operation.description {
Some(desc) -> sb |> se.doc_comment(desc)
_ -> sb
}
let has_params =
!list.is_empty(operation.parameters)
|| option.is_some(operation.request_body)
let sb = case has_params {
True ->
sb
|> se.line(
"pub fn "
<> fn_name
<> "(req: request_types."
<> request_type
<> ") -> response_types."
<> response_type
<> " {",
)
False ->
sb
|> se.line(
"pub fn " <> fn_name <> "() -> response_types." <> response_type <> " {",
)
}
let sb = case has_params {
True -> sb |> se.indent(1, "let _ = req")
False -> sb
}
sb
|> se.indent(1, "// TODO: Implement " <> fn_name)
|> se.indent(1, "todo")
|> se.line("}")
|> se.blank_line()
}
/// Generate a router module that dispatches requests.
fn generate_router(ctx: Context) -> String {
let sb =
se.file_header(context.version)
|> se.imports([
ctx.config.package <> "/handlers",
])
let sb =
sb
|> se.doc_comment("Route an incoming request to the appropriate handler.")
|> se.line("pub fn route(method: String, path: List(String)) -> String {")
|> se.indent(1, "case method, path {")
let operations = type_gen.collect_operations(ctx)
let sb =
list.fold(operations, sb, fn(sb, op) {
let #(op_id, _operation, path, method) = op
let fn_name = naming.operation_to_function_name(op_id)
let method_str = spec.method_to_string(method)
let path_pattern = path_to_pattern(path)
sb
|> se.indent(2, "\"" <> method_str <> "\", " <> path_pattern <> " -> {")
|> se.indent(3, "let _ = handlers." <> fn_name)
|> se.indent(3, "\"OK\"")
|> se.indent(2, "}")
})
let sb =
sb
|> se.indent(2, "_, _ -> \"Not Found\"")
|> se.indent(1, "}")
|> se.line("}")
|> se.blank_line()
se.to_string(sb)
}
/// Convert an OpenAPI path to a Gleam pattern match expression.
fn path_to_pattern(path: String) -> String {
let segments =
path
|> string_split("/")
|> list.filter(fn(s) { s != "" })
let patterns =
list.map(segments, fn(seg) {
case is_path_param(seg) {
True -> {
let param_name = extract_param_name(seg)
naming.to_snake_case(param_name)
}
False -> "\"" <> seg <> "\""
}
})
"[" <> se.join_with(patterns, ", ") <> "]"
}
/// Check if a path segment is a parameter.
fn is_path_param(segment: String) -> Bool {
case segment {
"{" <> _ -> True
_ -> False
}
}
/// Extract parameter name from {name}.
fn extract_param_name(segment: String) -> String {
segment
|> string_replace("{", "")
|> string_replace("}", "")
}
/// Simple string split (avoiding import issues).
fn string_split(input: String, delimiter: String) -> List(String) {
do_string_split(input, delimiter)
}
@external(erlang, "string", "split")
fn do_string_split(input: String, delimiter: String) -> List(String) {
// Fallback - won't be called on erlang target
let _ = delimiter
[input]
}
/// Simple string replace.
fn string_replace(input: String, pattern: String, replacement: String) -> String {
do_string_replace(input, pattern, replacement)
}
@external(erlang, "gleam_stdlib", "string_replace")
fn do_string_replace(
input: String,
pattern: String,
replacement: String,
) -> String {
let _ = pattern
let _ = replacement
input
}