///|
/// The keywords of Python 3.11. A field, a method or a parameter with one of
/// these names gets a `_` suffix.
let python_keywords : Array[String] = [
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
"if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise",
"return", "try", "while", "with", "yield",
]
///|
/// The attributes of a Pydantic `BaseModel`, and the names that the generated
/// classes use. A field with one of these names gets a `_` suffix.
let model_attributes : Array[String] = [
"construct", "copy", "dict", "from_orm", "json", "model_computed_fields", "model_config",
"model_construct", "model_copy", "model_dump", "model_dump_json", "model_extra",
"model_fields", "model_fields_set", "model_json_schema", "model_parametrized_name",
"model_post_init", "model_rebuild", "model_validate", "model_validate_json", "model_validate_strings",
"parse_file", "parse_obj", "parse_raw", "schema", "schema_json", "union_member",
"update_forward_refs", "validate",
]
///|
/// The names that the generated modules import or define. A shape with one of
/// these names gets a `_` suffix.
let python_type_names : Array[String] = [
"Annotated", "Any", "BaseModel", "BigDecimal", "BigInteger", "Blob", "Byte", "CallContext",
"Callable", "CancelRule", "Decimal", "FastAPI", "Integer", "Long", "Short", "TypeAlias",
"CapabilitySource", "ClassVar", "ConfigDict", "DateTimeTimestamp", "Discriminator",
"Document", "Double", "Enum", "EpochSecondsTimestamp", "Exception", "Field", "HttpDateTimestamp",
"IntEnum", "JsonValue", "MethodKind", "MethodRule", "ModeledError", "Peer", "Protocol",
"RpcError", "SessionRules", "StrEnum", "StrictBool", "Tag", "Timestamp", "TranscriptLog",
"TypeAdapter", "Unit",
]
///|
/// The words of a name. A word ends at `_`, `-`, `.`, `$`, at a change from a
/// lower-case letter or a digit to an upper-case letter, and before the last
/// upper-case letter of a run that a lower-case letter follows (`HTTPServer` is
/// `HTTP` and `Server`).
fn words(name : String) -> Array[String] {
let chars = name.to_array()
let out : Array[String] = []
let current = StringBuilder()
let flush = () => {
let word = current.to_string()
if !word.is_empty() {
out.push(word)
}
current.reset()
}
for index, c in chars {
if c == '_' || c == '-' || c == '.' || c == '$' || c == ' ' || c == '/' {
flush()
continue
}
if c.is_ascii_uppercase() && index > 0 {
let before = chars[index - 1]
let after_lower = index + 1 < chars.length() &&
chars[index + 1].is_ascii_lowercase()
if before.is_ascii_lowercase() ||
before.is_ascii_digit() ||
(before.is_ascii_uppercase() && after_lower) {
flush()
}
}
current.write_char(c)
}
flush()
out
}
///|
fn capitalize(word : String) -> String {
let chars = word.to_lower().to_array()
if chars.is_empty() {
return ""
}
chars[0] = chars[0].to_ascii_uppercase()
String::from_array(chars)
}
///|
/// A `snake_case` name for a field, a method or a function. A keyword or a
/// model attribute gets a `_` suffix. A name that starts with a digit gets an
/// `n` prefix.
pub fn snake_name(name : String) -> String {
let text = words(name).map(word => word.to_lower()).join("_")
let text = if text.is_empty() { "value" } else { text }
let text = if text.to_array()[0].is_ascii_digit() { "n" + text } else { text }
if python_keywords.contains(text) || model_attributes.contains(text) {
text + "_"
} else {
text
}
}
///|
/// The name of a handler or client method: a `snake_case` name. The client
/// keeps its peer in attribute `peer`, so a method `peer` gets a `_` suffix.
pub fn method_name(name : String) -> String {
let text = snake_name(name)
if text == "peer" {
text + "_"
} else {
text
}
}
///|
/// A `PascalCase` part of a class name, such as the member part of a union
/// member class. `manualEntry` is `ManualEntry`.
pub fn pascal_name(name : String) -> String {
let text = words(name).map(capitalize).join("")
let text = if text.is_empty() { "Value" } else { text }
if text.to_array()[0].is_ascii_digit() {
"V" + text
} else {
text
}
}
///|
/// An `UPPER_SNAKE` name for an enum member. `insertCard` is `INSERT_CARD`
/// and `USD` stays `USD`.
pub fn enum_member_name(name : String) -> String {
let text = words(name).map(word => word.to_upper()).join("_")
let text = if text.is_empty() { "VALUE" } else { text }
if text.to_array()[0].is_ascii_digit() {
"V_" + text
} else {
text
}
}
///|
/// The name of a generated class: the contextual name with an upper-case
/// first letter. A name that the generated modules use gets a `_` suffix.
pub fn class_name(name : String) -> String {
let chars = name.to_array()
let text = if chars.is_empty() {
"Value"
} else {
chars[0] = chars[0].to_ascii_uppercase()
String::from_array(chars)
}
if python_type_names.contains(text) || python_keywords.contains(text) {
text + "_"
} else {
text
}
}
///|
/// The text of a Python string literal in double quotes.
pub fn py_string(text : String) -> String {
let out = StringBuilder()
out.write_char('"')
for c in text {
match c {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
c if c.to_int() < 0x20 || c.to_int() == 0x7f => {
let hex = c.to_int().to_string(radix=16)
let padded = if hex.length() < 2 { "0" + hex } else { hex }
out.write_string("\\x" + padded)
}
c => out.write_char(c)
}
}
out.write_char('"')
out.to_string()
}
///|
/// The text of a docstring line: a backslash, a quote or a control character
/// becomes a space, so the line never ends or escapes the docstring.
fn doc_text(text : String) -> String {
let out = StringBuilder()
for c in text {
if c == '\\' || c == '"' || c.to_int() < 0x20 {
out.write_char(' ')
} else {
out.write_char(c)
}
}
out.to_string()
}
///|
/// I accept a Python package name: ASCII letters, digits and `_`, with a
/// letter or `_` first, and not a keyword.
fn check_package_name(text : String) -> Unit raise @codegen.CodegenError {
let chars = text.to_array()
let valid = !chars.is_empty() &&
(chars[0].is_ascii_alphabetic() || chars[0] == '_') &&
chars
.iter()
.all(c => c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_') &&
!python_keywords.contains(text)
if !valid {
raise @codegen.InvalidSettings(
"I expected setting `package` to be a Python package name such as `payment_terminal`, found `\{text}`.",
)
}
}
///|
/// The distribution name of a package for `pyproject.toml`: `_` becomes `-`.
fn distribution_name(package_name : String) -> String {
package_name.replace_all(old="_", new="-")
}