sparkles.base.text.case_style — Specification
Abstract
sparkles:base renames a single identifier from one naming convention into another — camel, Pascal, snake, kebab or screaming-snake case. It treats a run of capitals as one word (parsedJSON → ParsedJson), and a digit stays with the word it follows. The conversion runs at compile time as well as at run time, and it can write into a caller's buffer without allocating.
Introduction
Code that maps D identifiers onto an external format meets the same chore everywhere: a field named parsedJSON is spelled parsed_json in one format and ParsedJson in another. Done by hand, every serializer grows its own word splitter, and the splitters disagree on acronyms and digits.
This module owns that one transformation. It splits an identifier into words by case transitions and explicit separators, then rejoins the words in the chosen style. A letter followed by a digit does not split, so the digit joins the word before it, while an uppercase letter after a digit starts the next word (html5Parser → html5_parser). It is a format-agnostic text primitive: it has no serialization, UDA or per-member override concerns, which belong to policy layers such as sparkles:wired.
Section 2 lists the API, §3 and §4 define splitting and rejoining, §5 the compile-time contract, and §6 and §7 give runnable examples. The library overview is sparkles:base.
1. Overview
sparkles.base.text.case_style provides (re-exports, from sparkles:metadata, where the @WireCase attribute needs it) a CaseStyle enumeration and a compile-time-evaluable convertCase that renames a single identifier from one convention (e.g. fastPath) into another (e.g. fast_path, FAST_PATH, FastPath). It carries no opinion about where identifiers come from; callers pass in a string and get a string back.
| Identifier | Value |
|---|---|
| Dub sub-package | sparkles:base |
| Source root | libs/base/src/sparkles/base/text/ |
| Module | sparkles.base.text.case_style |
2. API surface
enum CaseStyle { original, camelCase, pascalCase, snakeCase, kebabCase, screamingSnakeCase }
// Writer form — the primitive: recase `ident` into an output range, no allocation.
void writeConvertedCase(CaseStyle style, Writer, R)(ref Writer w, R ident)
if (isOutputRange!(Writer, char) && isForwardRange!R && isSomeChar!(ElementType!R));
// Convenience — allocate and return the recased identifier.
string convertCase(CaseStyle style)(string ident); // CTFE-compatiblestyle is a template parameter, not a runtime argument, so both overloads can be instantiated and evaluated during CTFE (§5).
writeConvertedCase is the underlying primitive. It takes the input identifier as any forward range of character elements (a string and const(char)[] both qualify) and writes the recased result into the output range w element by element, allocating nothing of its own — suitable for @nogc paths that emit directly into a Buffer or a serializer's writer (§7).
convertCase is the allocating convenience wrapper: it builds the recased identifier into a fresh string and returns it, except for CaseStyle.original, which returns ident unchanged. It is defined in terms of writeConvertedCase over a string-building output range, so both forms produce identical text for the same style and identifier.
3. Word boundaries
convertCase first splits ident into words. A word boundary occurs at:
- a lowercase or digit immediately followed by an uppercase —
fastPathsplits intofast,Path; - an uppercase that begins a new word after an acronym run — an uppercase followed by an uppercase-then-lowercase —
JSONValuesplits intoJSON,Value; - an explicit separator (
_,-, or space), which is consumed and produces no word of its own.
Rules 1 and 2 keep acronym runs together (JSON, XML) while still separating the acronym from a following capitalized word. Rule 3 lets already-delimited identifiers (fast_path, fast-path, fast path) round-trip through the word splitter.
4. Rejoining per style
Once split into words, convertCase rejoins them per style:
CaseStyle | Transform | fromXMLToJSON → |
|---|---|---|
original | the identifier is returned unchanged (no split/rejoin) | fromXMLToJSON |
camelCase | lowercase the first word; title-case the rest; concatenate | fromXmlToJson |
pascalCase | title-case every word; concatenate | FromXmlToJson |
snakeCase | lowercase every word; join with _ | from_xml_to_json |
kebabCase | lowercase every word; join with - | from-xml-to-json |
screamingSnakeCase | uppercase every word; join with _ | FROM_XML_TO_JSON |
Title-casing a word capitalizes its first letter and lowercases the rest, so an acronym folds to title case: JSON → Json, XML → Xml. camelCase and pascalCase differ only in the first word (jsonValue vs JsonValue).
5. Compile-time evaluation
RECASE1: CTFE usability. convertCase must be usable during CTFE without requiring the identifier itself to be a template argument.
Rationale: Consumers derive names at compile time, for example an enum member's wire name from __traits(identifier, …) inside a static foreach.
RECASE2: Compile-time coverage. The implementation must include static assert coverage for every CaseStyle, including acronym, digit, and explicit-separator cases.
6. Examples
Every style for a handful of identifiers, including acronym runs:
#!/usr/bin/env dub
/+ dub.sdl:
name "case_style_table"
dependency "sparkles:base" version="*"
+/
import std.stdio : writefln;
import sparkles.base.text.case_style : CaseStyle, convertCase;
void main()
{
static foreach (id; ["fastPath", "parsedJSON", "JSONValue", "fromXMLToJSON"])
writefln("%s -> snake:%s kebab:%s scream:%s pascal:%s",
id,
convertCase!(CaseStyle.snakeCase)(id),
convertCase!(CaseStyle.kebabCase)(id),
convertCase!(CaseStyle.screamingSnakeCase)(id),
convertCase!(CaseStyle.pascalCase)(id));
}fastPath -> snake:fast_path kebab:fast-path scream:FAST_PATH pascal:FastPath
parsedJSON -> snake:parsed_json kebab:parsed-json scream:PARSED_JSON pascal:ParsedJson
JSONValue -> snake:json_value kebab:json-value scream:JSON_VALUE pascal:JsonValue
fromXMLToJSON -> snake:from_xml_to_json kebab:from-xml-to-json scream:FROM_XML_TO_JSON pascal:FromXmlToJsonoriginal returns the identifier verbatim, while camelCase lowercases only the leading word:
#!/usr/bin/env dub
/+ dub.sdl:
name "case_style_camel"
dependency "sparkles:base" version="*"
+/
import std.stdio : writefln;
import sparkles.base.text.case_style : CaseStyle, convertCase;
void main()
{
static foreach (id; ["fastPath", "JSONValue", "fromXMLToJSON"])
writefln("%s -> original:%s camel:%s", id,
convertCase!(CaseStyle.original)(id),
convertCase!(CaseStyle.camelCase)(id));
}fastPath -> original:fastPath camel:fastPath
JSONValue -> original:JSONValue camel:jsonValue
fromXMLToJSON -> original:fromXMLToJSON camel:fromXmlToJson7. Writer form
writeConvertedCase recases directly into an output range without allocating an intermediate string, so it composes with the @nogc writers of sparkles.base.text and with a UniqueBuffer sink. The recasing call itself is @safe pure nothrow @nogc:
#!/usr/bin/env dub
/+ dub.sdl:
name "case_style_writer"
dependency "sparkles:base" version="*"
+/
import std.stdio : writeln;
import sparkles.base.buffer : UniqueBuffer;
import sparkles.base.text.case_style : CaseStyle, writeConvertedCase;
void main()
{
UniqueBuffer!(char, 64) buf;
writeConvertedCase!(CaseStyle.snakeCase)(buf, "fromXMLToJSON");
buf ~= ' ';
writeConvertedCase!(CaseStyle.pascalCase)(buf, "parsedJSON");
writeln(buf[]);
}from_xml_to_json ParsedJsonBecause it accepts any forward range of characters, the input need not be a string literal; a slice, a const(char)[], or another character range works too. convertCase!style(ident) (§2) is exactly this writer targeting a string-building range, so the two never disagree.
→ sparkles.base.text cell-splitting & width spec — the sibling text spec → sparkles:wired — a policy layer that consumes convertCase → sparkles:base — the library overview