Current section
Files
Jump to
Current section
Files
src/sqlode/internal/codegen/params.gleam
import gleam/list
import gleam/option
import gleam/string
import sqlode/internal/codegen/common
import sqlode/internal/model
import sqlode/internal/naming
import sqlode/internal/type_mapping
pub fn render(
naming_ctx: naming.NamingContext,
queries: List(model.AnalyzedQuery),
type_mapping: model.TypeMapping,
module_path: String,
runtime_import: String,
) -> String {
let has_slices = common.queries_have_slices(queries)
let has_arrays = queries_have_array_params(queries)
let has_enums = common.queries_have_enum_params(queries)
let custom_imports =
common.param_scalar_types(queries)
|> common.custom_type_imports
// Both `strong` and `rich` mappings qualify rich scalars through
// `models.<Alias|Wrapper>`. Under `strong` mapping the encoder
// also references `models.<type>_to_string(...)` to unwrap values
// before handing them to the runtime. Under `rich` mapping the
// aliases compile to plain `String` at runtime, but the
// `models.` qualifier in the record field types still needs the
// module import.
let needs_models_for_rich =
{ type_mapping == model.StrongMapping || type_mapping == model.RichMapping }
&& list.any(queries, fn(q) {
list.any(q.params, fn(p) { type_mapping.is_rich_type(p.scalar_type) })
})
let runtime_import_line = "import " <> runtime_import <> ".{type Value}"
let option_import = case needs_option_import(queries) {
True -> ["import gleam/option.{type Option}"]
False -> []
}
let imports =
list.flatten([
case has_slices || has_arrays {
True -> ["import gleam/list"]
False -> []
},
option_import,
[runtime_import_line],
case has_enums || needs_models_for_rich {
True -> ["import " <> module_path <> "/models"]
False -> []
},
custom_imports,
])
let queries_with_params =
list.filter(queries, fn(q) { !list.is_empty(q.params) })
let declarations =
queries_with_params
|> list.map(render_params_declaration(naming_ctx, _, type_mapping))
|> string.join("\n\n")
let effective_imports = case queries_with_params {
[] -> []
_ -> imports
}
let lines =
list.flatten([
["// Code generated by sqlode. DO NOT EDIT.", ""],
effective_imports,
case declarations {
"" -> []
_ -> ["", declarations]
},
])
string.join(lines, "\n")
}
fn queries_have_array_params(queries: List(model.AnalyzedQuery)) -> Bool {
list.any(queries, fn(q) {
list.any(q.params, fn(p) {
case p.scalar_type {
model.ArrayType(_) -> True
_ -> False
}
})
})
}
fn needs_option_import(queries: List(model.AnalyzedQuery)) -> Bool {
list.any(queries, fn(query) {
list.any(query.params, fn(param) { param.nullable })
})
}
fn render_params_declaration(
naming_ctx: naming.NamingContext,
query: model.AnalyzedQuery,
type_mapping: model.TypeMapping,
) -> String {
let type_name = naming.to_pascal_case(naming_ctx, query.base.name) <> "Params"
let function_name = query.base.function_name <> "_values"
case query.params {
[] -> ""
params -> {
let has_slices = common.has_slices(params)
let values_body = case has_slices {
True ->
" list.flatten(["
<> string.join(param_accessors_flattened(params, type_mapping), ", ")
<> "])"
False ->
" ["
<> string.join(param_accessors(params, type_mapping), ", ")
<> "]"
}
string.join(
[
"pub type " <> type_name <> " {",
" "
<> type_name
<> "("
<> string.join(param_fields(params, type_mapping), ", ")
<> ")",
"}",
"",
"pub fn "
<> function_name
<> "(params: "
<> type_name
<> ") -> List(Value) {",
values_body,
"}",
],
"\n",
)
}
}
}
fn param_fields(
params: List(model.QueryParam),
type_mapping: model.TypeMapping,
) -> List(String) {
params
|> list.map(fn(param) {
let base_type = common.qualified_field_type(param.scalar_type, type_mapping)
let typed = case param.is_list {
True -> "List(" <> base_type <> ")"
False ->
case param.nullable {
True -> "Option(" <> base_type <> ")"
False -> base_type
}
}
param.field_name <> ": " <> typed
})
}
fn param_accessors(
params: List(model.QueryParam),
type_mapping: model.TypeMapping,
) -> List(String) {
params
|> list.map(render_param_value(_, type_mapping))
}
// ============================================================
// ValueEncoder — abstract encoding strategy
// ============================================================
/// Describes how a parameter value should be encoded, separating
/// the *what* (encoding strategy) from the *how* (Gleam source rendering).
type ValueEncoder {
/// A direct runtime call: `runtime.string(value)`
DirectEncode(runtime_fn: String)
/// Unwrap through a model function first: `runtime.int(models.unwrap(value))`
UnwrapEncode(runtime_fn: String, unwrap_fn: String)
/// Encode an enum: `runtime.string(models.status_to_string(value))`
EnumEncode(runtime_fn: String, to_string_fn: String)
/// Pipe the value through a user-provided codec hook before the
/// runtime encoder: `runtime.int(types.user_id_to_int(value))`. Used
/// for `CustomType` overrides that opted into explicit codec hooks
/// (issue #529 — opaque domain types). The hook is rendered
/// module-qualified when the type's `gleam_type` carried a module
/// prefix, matching the import shape of `custom_type_imports`.
HookEncode(runtime_fn: String, hook_call: String)
/// Encode an array: `runtime.array(list.map(value, element_encoder))`
ArrayEncode(element_encoder: ValueEncoder)
}
/// Determine the encoding strategy for a scalar type.
fn plan_encoding(
scalar_type: model.ScalarType,
type_mapping_mode: model.TypeMapping,
) -> ValueEncoder {
case scalar_type {
model.EnumType(name) -> {
let runtime_fn = type_mapping.scalar_type_to_runtime_function(scalar_type)
let to_str = "models." <> type_mapping.enum_to_string_fn(name)
EnumEncode(runtime_fn:, to_string_fn: to_str)
}
// SetType reuses the EnumEncode shape: project the
// `List(<Name>Value)` field through `<name>_set_to_string` (the
// helper emitted by `codegen/models.gleam`) before handing the
// resulting comma-joined `String` to `runtime.string`.
model.SetType(name) -> {
let runtime_fn = type_mapping.scalar_type_to_runtime_function(scalar_type)
let to_str = "models." <> type_mapping.set_to_string_fn(name)
EnumEncode(runtime_fn:, to_string_fn: to_str)
}
model.ArrayType(element) -> {
let inner = plan_encoding(element, type_mapping_mode)
ArrayEncode(element_encoder: inner)
}
// Custom override with explicit codec hooks (issue #529): pipe
// the user value through the encode hook before the underlying
// runtime encoder. Branches above ArrayType / EnumType fall
// through to the legacy paths so wrapped collections still work.
model.CustomType(module:, codec: option.Some(hooks), ..) -> {
let runtime_fn = type_mapping.scalar_type_to_runtime_function(scalar_type)
let hook_call = qualified_hook_call(module, hooks.encode)
HookEncode(runtime_fn:, hook_call:)
}
_ -> {
let runtime_fn = type_mapping.scalar_type_to_runtime_function(scalar_type)
case type_mapping_mode {
model.StrongMapping ->
case type_mapping.strong_type_unwrap_fn(scalar_type) {
option.Some(fn_name) ->
UnwrapEncode(runtime_fn:, unwrap_fn: "models." <> fn_name)
option.None -> DirectEncode(runtime_fn:)
}
_ -> DirectEncode(runtime_fn:)
}
}
}
}
/// Build a callable expression for a codec hook function.
///
/// When the type carried a module prefix (e.g. `myapp/types.UserId`),
/// the existing `import myapp/types.{type UserId}` line in the
/// generated file binds `types` as a module alias, so we produce
/// `types.<fn_name>`. When the type has no module prefix the user is
/// expected to ensure the function is reachable in the generated
/// file's scope; we emit the bare name so any import strategy works.
fn qualified_hook_call(module: option.Option(String), fn_name: String) -> String {
case module {
option.Some(module_path) -> module_alias_for(module_path) <> "." <> fn_name
option.None -> fn_name
}
}
fn module_alias_for(module_path: String) -> String {
case string.split(module_path, "/") {
[] -> module_path
segments ->
case list.last(segments) {
Ok(last) -> last
Error(_) -> module_path
}
}
}
/// Render the encoder as a function expression (for use in callbacks).
fn render_encoder_fn(encoder: ValueEncoder) -> String {
case encoder {
DirectEncode(runtime_fn:) -> runtime_fn
UnwrapEncode(runtime_fn:, unwrap_fn:) ->
"fn(v) { " <> runtime_fn <> "(" <> unwrap_fn <> "(v)) }"
EnumEncode(runtime_fn:, to_string_fn:) ->
"fn(v) { " <> runtime_fn <> "(" <> to_string_fn <> "(v)) }"
HookEncode(runtime_fn:, hook_call:) ->
"fn(v) { " <> runtime_fn <> "(" <> hook_call <> "(v)) }"
ArrayEncode(element_encoder:) -> {
let mapper = render_encoder_fn(element_encoder)
"fn(v) { runtime.array(list.map(v, " <> mapper <> ")) }"
}
}
}
/// Render a direct application of the encoder to a value expression.
fn render_encoder_apply(encoder: ValueEncoder, value_expr: String) -> String {
case encoder {
DirectEncode(runtime_fn:) -> runtime_fn <> "(" <> value_expr <> ")"
UnwrapEncode(runtime_fn:, unwrap_fn:) ->
runtime_fn <> "(" <> unwrap_fn <> "(" <> value_expr <> "))"
EnumEncode(runtime_fn:, to_string_fn:) ->
runtime_fn <> "(" <> to_string_fn <> "(" <> value_expr <> "))"
HookEncode(runtime_fn:, hook_call:) ->
runtime_fn <> "(" <> hook_call <> "(" <> value_expr <> "))"
ArrayEncode(element_encoder:) -> {
let mapper = render_encoder_fn(element_encoder)
"runtime.array(list.map(" <> value_expr <> ", " <> mapper <> "))"
}
}
}
fn render_param_value(
param: model.QueryParam,
type_mapping_mode: model.TypeMapping,
) -> String {
let value_expr = "params." <> param.field_name
let encoder = plan_encoding(param.scalar_type, type_mapping_mode)
case param.nullable {
True -> {
let encode_fn = render_encoder_fn(encoder)
"runtime.nullable(" <> value_expr <> ", " <> encode_fn <> ")"
}
False -> render_encoder_apply(encoder, value_expr)
}
}
/// Render param values for queries with slice params.
/// Scalars are wrapped in singleton lists, slices are mapped.
fn param_accessors_flattened(
params: List(model.QueryParam),
type_mapping: model.TypeMapping,
) -> List(String) {
params
|> list.map(fn(param) {
case param.is_list {
True -> render_list_param_mapper(param, type_mapping)
False -> "[" <> render_param_value(param, type_mapping) <> "]"
}
})
}
fn render_list_param_mapper(
param: model.QueryParam,
type_mapping_mode: model.TypeMapping,
) -> String {
let value_expr = "params." <> param.field_name
let encoder = plan_encoding(param.scalar_type, type_mapping_mode)
let mapper = render_encoder_fn(encoder)
"list.map(" <> value_expr <> ", " <> mapper <> ")"
}