Packages
oaspec
0.11.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/openapi/dedup.gleam
import gleam/dict.{type Dict}
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import oaspec/openapi/schema.{
type SchemaObject, type SchemaRef, AnyOfSchema, Forbidden, Inline,
ObjectSchema, OneOfSchema, Reference, Typed, Untyped,
}
import oaspec/openapi/spec.{
type OpenApiSpec, type Operation, type PathItem, Components, OpenApiSpec,
PathItem, Ref, Value,
}
import oaspec/util/naming
/// Deduplicate names in the spec to avoid collisions in generated code.
/// This is a pre-processing pass that runs after hoisting and before validation.
/// It handles:
/// - Duplicate operationIds across operations
/// - Function/type name collisions after case conversion of operationIds
/// Property name and enum variant deduplication is done at codegen time via
/// dedup_property_names/1 and dedup_enum_variants/1 to preserve JSON wire names.
pub fn dedup(spec: OpenApiSpec(stage)) -> OpenApiSpec(stage) {
let spec = dedup_operation_ids(spec)
let spec = dedup_schemas(spec)
spec
}
/// Deduplicate operationIds across all operations.
/// If two operations have the same operationId, the second gets "_2" appended, etc.
/// Also handles function/type name collisions after case conversion.
fn dedup_operation_ids(spec: OpenApiSpec(stage)) -> OpenApiSpec(stage) {
// Collect all operation IDs with their paths
let all_ops = collect_all_operations(spec)
// First pass: deduplicate raw operationIds
let raw_ids =
list.map(all_ops, fn(op) {
let #(_path, _method, operation) = op
case operation.operation_id {
Some(id) -> id
None -> ""
}
})
let deduped_ids = deduplicate_strings(raw_ids)
// Second pass: deduplicate after snake_case conversion (function names)
let fn_names = list.map(deduped_ids, naming.operation_to_function_name)
let deduped_fn_names = deduplicate_strings(fn_names)
// Build mapping: index -> final operationId
// If function name was deduped, derive the operationId from the deduped function name
let indexed_ops = list.index_map(all_ops, fn(op, idx) { #(idx, op) })
let id_map =
list.index_fold(indexed_ops, dict.new(), fn(acc, entry, _) {
let #(idx, #(path, method, _op)) = entry
let final_id = case
list_at(deduped_ids, idx),
list_at(deduped_fn_names, idx)
{
Some(raw_id), Some(fn_name) -> {
// If function name differs from snake_case of raw_id, use the deduped fn name
let expected_fn = naming.operation_to_function_name(raw_id)
case expected_fn == fn_name {
True -> raw_id
False -> fn_name
}
}
Some(raw_id), _ -> raw_id
_, _ -> ""
}
dict.insert(acc, #(path, method), final_id)
})
// Apply the deduped IDs back to the spec
let new_paths =
dict.to_list(spec.paths)
|> list.map(fn(entry) {
let #(path, ref_or) = entry
case ref_or {
Value(path_item) -> {
let new_item = dedup_path_item_ops(path_item, path, id_map)
#(path, Value(new_item))
}
Ref(_) as r -> #(path, r)
}
})
|> dict.from_list()
OpenApiSpec(..spec, paths: new_paths)
}
fn dedup_path_item_ops(
item: PathItem(stage),
path: String,
id_map: Dict(#(String, String), String),
) -> PathItem(stage) {
PathItem(
..item,
get: apply_deduped_id(item.get, path, "get", id_map),
post: apply_deduped_id(item.post, path, "post", id_map),
put: apply_deduped_id(item.put, path, "put", id_map),
delete: apply_deduped_id(item.delete, path, "delete", id_map),
patch: apply_deduped_id(item.patch, path, "patch", id_map),
head: apply_deduped_id(item.head, path, "head", id_map),
options: apply_deduped_id(item.options, path, "options", id_map),
trace: apply_deduped_id(item.trace, path, "trace", id_map),
)
}
fn apply_deduped_id(
op: Option(Operation(stage)),
path: String,
method: String,
id_map: Dict(#(String, String), String),
) -> Option(Operation(stage)) {
case op {
None -> None
Some(operation) ->
case dict.get(id_map, #(path, method)) {
Ok(new_id) if new_id != "" ->
Some(spec.Operation(..operation, operation_id: Some(new_id)))
_ -> Some(operation)
}
}
}
/// Collect all operations as (path, method_str, operation) tuples.
fn collect_all_operations(
spec: OpenApiSpec(stage),
) -> List(#(String, String, Operation(stage))) {
let paths =
list.sort(dict.to_list(spec.paths), fn(a, b) { string.compare(a.0, b.0) })
list.flat_map(paths, fn(entry) {
let #(path, ref_or) = entry
case ref_or {
Ref(_) -> []
Value(item) -> {
let ops = [
#("get", item.get),
#("post", item.post),
#("put", item.put),
#("delete", item.delete),
#("patch", item.patch),
#("head", item.head),
#("options", item.options),
#("trace", item.trace),
]
list.filter_map(ops, fn(op) {
case op {
#(method, Some(operation)) -> Ok(#(path, method, operation))
_ -> Error(Nil)
}
})
}
}
})
}
/// Recurse into nested schemas within components (e.g. oneOf children).
fn dedup_schemas(spec: OpenApiSpec(stage)) -> OpenApiSpec(stage) {
case spec.components {
None -> spec
Some(components) -> {
let new_schemas =
dict.to_list(components.schemas)
|> list.map(fn(entry) {
let #(name, schema_ref) = entry
#(name, dedup_schema_ref(schema_ref))
})
|> dict.from_list()
OpenApiSpec(
..spec,
components: Some(Components(..components, schemas: new_schemas)),
)
}
}
}
fn dedup_schema_ref(schema_ref: SchemaRef) -> SchemaRef {
case schema_ref {
Reference(..) -> schema_ref
Inline(schema_obj) -> Inline(dedup_schema_object(schema_obj))
}
}
fn dedup_schema_object(schema_obj: SchemaObject) -> SchemaObject {
case schema_obj {
ObjectSchema(properties:, additional_properties:, ..) as obj -> {
// Only recurse into child schemas — do NOT rename property keys.
let new_props =
dict.to_list(properties)
|> list.map(fn(entry) {
let #(name, prop_ref) = entry
#(name, dedup_schema_ref(prop_ref))
})
|> dict.from_list()
ObjectSchema(
..obj,
properties: new_props,
additional_properties: case additional_properties {
Typed(ap) -> Typed(dedup_schema_ref(ap))
Forbidden -> Forbidden
Untyped -> Untyped
},
)
}
// Do NOT rename enum values — they are JSON wire values.
// Gleam variant deduplication is handled at codegen time via
// dedup_enum_variants/1.
OneOfSchema(metadata:, schemas:, discriminator:) ->
OneOfSchema(
metadata:,
schemas: list.map(schemas, dedup_schema_ref),
discriminator:,
)
AnyOfSchema(metadata:, schemas:, discriminator:) ->
AnyOfSchema(
metadata:,
schemas: list.map(schemas, dedup_schema_ref),
discriminator:,
)
_ -> schema_obj
}
}
/// Given a list of original property names (JSON wire names), return a list
/// of deduped snake_case Gleam field names. The returned list is parallel
/// to the input: result[i] is the Gleam name for input[i].
pub fn dedup_property_names(prop_names: List(String)) -> List(String) {
let snake_names = list.map(prop_names, naming.to_snake_case)
deduplicate_strings(snake_names)
}
/// Given a list of original enum values (JSON wire values), return a list
/// of deduped PascalCase Gleam variant suffixes. The returned list is
/// parallel to the input.
pub fn dedup_enum_variants(enum_values: List(String)) -> List(String) {
let pascal_names = list.map(enum_values, naming.to_pascal_case)
deduplicate_strings(pascal_names)
}
/// Deduplicate a list of strings by appending "_2", "_3", etc. for duplicates.
fn deduplicate_strings(names: List(String)) -> List(String) {
let #(result_rev, _) =
list.fold(names, #([], dict.new()), fn(acc, name) {
let #(result, counts) = acc
case dict.get(counts, name) {
Error(_) -> {
let counts = dict.insert(counts, name, 1)
#([name, ..result], counts)
}
Ok(count) -> {
let new_count = count + 1
let unique_name = name <> "_" <> int.to_string(new_count)
let counts = dict.insert(counts, name, new_count)
#([unique_name, ..result], counts)
}
}
})
list.reverse(result_rev)
}
/// Get element at index from a list.
fn list_at(lst: List(a), idx: Int) -> Option(a) {
case lst, idx {
[], _ -> None
[head, ..], 0 -> Some(head)
[_, ..rest], n -> list_at(rest, n - 1)
}
}