Library / SDK
comma-csv/comma avatar
comma-csv/comma

comma: a small DSL for CSV output in Ruby

Comma is a small CSV (ie. comma separated values) generation extension for Ruby objects, that lets you seamlessly define a CSV output format via a small DSL

520 stars75 forksRubyMIT

At a glance

What is it?
Comma is a Ruby library that turns objects, arrays and ORM collections into CSV through a small block based DSL, with named styles for alternative formats, a documented pattern for columns that are only known at runtime, and a Rails integration that adds render csv to your controllers. It is a small, MIT licensed gem that recently shipped a new major version.
Who is it for?
comma fits a Rails or plain Ruby application whose export format is defined in one place next to the model, because the DSL keeps the column list and the header labels in the class rather than in a controller or a template, and the dynamic column pattern in the documentation solves the awkward case of a per-tenant export.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 25 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A gem that describes a CSV format where the object lives

Comma is a library for generating comma-separated values from Ruby objects, arrays and supported ORM collections, and it describes itself as a small CSV generation extension that lets you define a CSV output format through a small DSL. The homepage is the repository itself, the licence is MIT, and the authors are listed as Marcus Crafter and Tom Meier for the initial work, with Eito Katagiri also credited.

The requirement is Ruby 3.1 or later, and the Rails integration has a specific support matrix rather than a vague one. The repository is currently tested against ActiveRecord and Rails 7.1 and 7.2 on Ruby 3.1 through 4.0, and against Rails 8.0 and 8.1 on Ruby 3.2 through 4.0, with the note that Rails 8.x requires Ruby 3.2 or newer.

The versioning is SemVer, and the release history shows the project moving quickly: version 5.1.0 on 2026-09-06, version 5.0.0 on 2026-08-22 and version 4.9.0 on 2026-05-17, with the last push on 2026-09-28 and the default branch called master. So there was a major version in the last couple of months, which for a small gem is a sign that the API is still being shaped, and a reason to pin the version in your Gemfile rather than float on the major.

The installation is the standard Bundler route, with the gem declared in the Gemfile, and the project also offers the main branch directly for anyone who wants to live on the edge.

The DSL is a block of method calls on the class

The whole idea fits in one class. You call comma on the class and list the columns, and a bare method call is a column whose header is derived from the method name, while a method call with a string argument sets the header explicitly.

ruby
class User
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  comma do
    first_name
    last_name
    full_name 'Name'
  end
end

And then the output comes from calling to_comma, which behaves differently depending on what you call it on:

ruby
user = User.new('Ada', 'Lovelace')
user.to_comma
# => ["Ada", "Lovelace", "Ada Lovelace"]

users = [User.new('Ada', 'Lovelace'), User.new('Grace', 'Hopper')]
users.to_comma
# => "First name,Last name,Name\nAda,Lovelace,Ada Lovelace\nGrace,Hopper,Grace Hopper\n"

A single object gives you the row values as an array. A collection gives you the whole CSV, with a header row derived from the column names, which is why the header for the first column is First name rather than first_name.

That dual behaviour is convenient and also worth knowing about, because a caller who expects an array from to_comma and gets a string on a collection, or the other way round, will produce a confusing bug. The format is defined once and applies to every instance of the class, which is the point: the export shape is a property of the model rather than of the code that exports it.

Named styles, for the exports that are not the default one

The same class often needs more than one output format, and Comma handles that with named styles. A style is a second comma block with a name:

ruby
class User
  comma :short do
    first_name
    last_name
  end
end

users.to_comma(:short)

The style is selected when the CSV is generated, so the same objects can be exported in a full format for an internal report and a short format for a partner. The default format is the one declared without a name.

The library then goes further than a plain column list, and the documentation shows a pattern for columns that are only known at runtime, which is the case when the export format comes from tenant configuration. The rule it states is specific: formats are evaluated separately to produce headers and to produce each row, so for a CSV whose columns are configured at runtime you must resolve and order the full field definition list before declaring the comma format, and keep that format isolated to the current export so that another export cannot redefine it midway through generation.

The example that follows is longer than a typical README snippet, and it is worth reading because it shows the shape of the solution. It defines two small Struct classes, one for a field with a key and a label and one for a value with a field key and a value, and then a User that holds a name and a set of dynamic values keyed by field key. The export function creates a row class with Struct.new, attaches a style called :export to it, and inside the style block uses __static_column__ with a block for the name and then loops over the resolved fields, using field.label for the header and row.user.dynamic_value(field.key) for the value.

The point of the exercise is that the headers and the values are generated by two separate passes, so a field list that changes between them would misalign the output. Resolving the list once, before the format is declared, and building the row class inside the export function so that each export gets its own class, are the two things that keep a concurrent export from overwriting the format in use.

Rails gets a render csv option

In a Rails controller the integration is one line, because requiring the gem registers render csv support:

ruby
def index
  render csv: User.all
end

That is the whole integration. The collection goes in, the format defined on the model is used, and the CSV comes out, with the header row and the values derived from the comma block. There is no separate presenter object, no view template and no options object to pass, which is the appeal for an application whose export is a list of a model's columns.

The ActiveRecord support is the reason the library exists in its current form, since a collection of ActiveRecord objects is exactly the case the collection path is designed for, and the version matrix in the README exists to say which combinations of ActiveRecord, Rails and Ruby are actually tested.

For a plain Ruby application, the same to_comma call works on an array of your own objects, and the gem has no Rails dependency for that path, since the Rails behaviour is added by the require side effect rather than by the core.

The wiki linked from the README holds more usage examples, and for a library this small the wiki is likely to be more useful than the README for the second and third use case, because the README concentrates on the features that changed most recently.

Running the tests across a Rails matrix

The contributor workflow is documented in enough detail that you can reproduce it, which is a good sign for a library with a version matrix.

The dependencies install with Bundler and Appraisal:

sh
bundle install
bundle exec appraisal install

The default suite and the linter are two commands:

sh
bundle exec rspec spec
bundle exec rubocop -P

A single example can be run by giving the file and the line number, which is the RSpec convention:

sh
bundle exec rspec spec/comma/comma_spec.rb:205

And the full matrix across the Rails and ActiveRecord gemfiles runs through Appraisal, which is the tool from Thoughtbot for testing a library against several dependency sets:

sh
bundle exec appraisal rake spec

A specific spec can be run under one appraisal as well, for example the rails7.1.6 appraisal against a controller spec, which is what you would do when a failure only appears in one Rails version.

The contributing section asks that the rspec, the rubocop and any relevant appraisal commands pass before a pull request is opened, which is a stricter requirement than most small projects impose, and it is the right one when the library supports a matrix of Rails versions.

The repository layout supports that workflow: an Appraisals file, a gemfiles/ directory holding the generated gemfiles for each combination, a .coveralls.yml for coverage reporting, a .travis.yml, a .rubocop.yml with a .rubocop_todo.yml of accepted offences, an init.rb, a Rakefile, a comma.gemspec and a docs/ directory.

Editorial conclusion

comma fits a Rails or plain Ruby application whose export format is defined in one place next to the model, because the DSL keeps the column list and the header labels in the class rather than in a controller or a template, and the dynamic column pattern in the documentation solves the awkward case of a per-tenant export. It is the wrong tool for a one-off export, where the standard library CSV module and a few lines of code are simpler, and the README does not document any streaming support for very large files. Before you adopt it, check the Ruby version, 3.1 or later, and the Rails version against the supported matrix, since the gem is tested against specific combinations, and if your columns are dynamic, follow the documented pattern of resolving the field list before declaring the format so that headers and rows cannot disagree.

Frequently asked questions

What does the comma gem do in Ruby?

Comma generates CSV from Ruby objects, arrays and supported ORM collections through a small DSL. You declare a comma block on the class listing the columns, a bare method call becomes a column with a derived header, and a call with a string sets the header explicitly. Calling to_comma on one object returns the row values as an array, and calling it on a collection returns the full CSV with a header row.

How do I define more than one CSV format for the same model in comma?

You declare a named style with a second comma block, for example comma :short do with the columns you want, and select it when generating the output with to_comma(:short). The style declared without a name is the default format.

How does comma handle columns that are only known at runtime?

The documentation says to resolve and order the full field definition list before declaring the comma format, and to keep that format isolated to the current export so another export cannot redefine it midway. The example uses a row class created inside the export function, __static_column__ with a block for each field, field.label for the header and field.key to read the value, so headers and rows are generated from the same list.

How do I export CSV from a Rails controller with comma?

Requiring the gem registers render csv support, so the controller action is a single line: render csv: User.all. The collection is exported using the format declared on the model, and the supported combinations are ActiveRecord and Rails 7.1 and 7.2 on Ruby 3.1 through 4.0, and Rails 8.0 and 8.1 on Ruby 3.2 through 4.0.

Official sources

  1. comma-csv/comma on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/comma-csv-comma.svg)](https://hysenlabs.com/projects/comma-csv-comma)

Was this useful?