This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
csv_generator is a published Hex library (no application, no supervision tree) that provides a compile-time DSL for defining CSV layouts. Users use CsvGenerator, declare columns with macros, and get a generated render/1 function on their module.
mix deps.get
mix test # runs with elixirc_paths ["lib", "test"]
mix test test/csv_generator_test.exs:22 # single test by line number
mix format # run after every edit to an .ex/.exs file
mix docs # ex_doc; dev/docs env also compiles docs/CI (.github/workflows/elixir.yml) runs only mix test on push/PR to main.
Everything happens at compile time in lib/csv_generator.ex — there is no runtime state.
__using__/1registers module attributes (@columnsaccumulating, plus@delimiter,@line_ending,@decimal_point,@no_header) and hooks@before_compile.- The DSL macros (
column/3,hardcoded/3,delimiter/1,line_ending/1,decimal_point/1,header/1) only push{name, type, opts}tuples onto@columnsor set a scalar attribute. Validation happens in the macro — invalid types/argumentsraise ArgumentErrorat compile time using__CALLER__.line. __before_compile__/1reads the attributes back (reversing@columns, since accumulate prepends) and callscompile/5.compile/5builds the whole AST: a per-columnrender/2clause (fromgen_columns/2), a static header line (gen_header/2), and the publicrender/1that maps rows → columns → joins.
Each column generates a render(:column_name, value) clause pattern-matched on the column's atom name. This is why:
- Column names must be unique — use
source:to emit the same input field twice under different names/formats. hardcoded/3synthesises names like:hardcoded0,:hardcoded1fromlength(@columns).- Options like
format:,digits:, anddecimal_pointare baked into the generated clause body, not looked up at runtime.
When a column has with:, gen_columns/2 emits the type-specific clause as post_render/2 instead of render/2, and adds a render(name, value) wrapper that calls the user function first, then post_render. Anonymous functions survive being stored in a module attribute because column/3 Macro.escapes the :with value before accumulating it.
nilrenders as""for every type except:string, which renders""(quoted empty).:stringvalues go throughto_string/1and have"doubled per RFC 4180.:floatwithdigits:adds5 / 10^(digits+2)before:erlang.float_to_binary/2to force round-half-up rather than banker's rounding.header/1stores the negation into@no_header(header false→@no_header true).- All scalar attributes use
|| defaultso an explicitnilfalls back to the default.
@moduledoc File.read!("README.md")— the README is the module docs. Changes to the DSL must be reflected inREADME.md, not in a separate moduledoc..formatter.exsexportslocals_without_parensfor the DSL macros. Adding a new DSL macro means adding it there too, or user code formats badly.- Adding a type means updating
@types, thecase type doingen_columns/2, and the type table incolumn/3's@doc. - Bump
@versioninmix.exsand add aCHANGELOG.mdentry for any released change;package/0files:controls what ships to Hex. docs/example.exis compiled only in:dev/:docs; it exists purely to render an example page in the docs.