TableStructure::Schema
- Defines columns of a table using DSL.
TableStructure::Writer
- Converts data with the schema, and outputs table structured data.
TableStructure::Iterator
- Converts data with the schema, and enumerates table structured data.
TableStructure::Table
- Provides methods for converting data with the schema.
Add this line to your application's Gemfile:
gem 'table_structure'
And then execute:
$ bundle
Or install it yourself as:
$ gem install table_structure
Define a schema:
class SampleTableSchema
include TableStructure::Schema
column name: 'ID',
value: ->(row, _table) { row[:id] }
column name: 'Name',
value: ->(row, *) { row[:name] }
column name: ['Pet 1', 'Pet 2', 'Pet 3'],
value: ->(row, *) { row[:pets] }
columns do |table|
table[:questions].map do |question|
{
name: question[:id],
value: ->(row, *) { row[:answers][question[:id]] }
}
end
end
column_builder :to_s do |val, _row, _table|
val.to_s
end
end
Initialize the schema:
context = {
questions: [
{ id: 'Q1', text: 'Do you like sushi?' },
{ id: 'Q2', text: 'Do you like yakiniku?' },
{ id: 'Q3', text: 'Do you like ramen?' }
]
}
schema = SampleTableSchema.new(context: context)
Initialize a writer with the schema:
writer = TableStructure::Writer.new(schema)
## To omit header, write:
# writer = TableStructure::Writer.new(schema, header: false)
Write the items converted by the schema to array:
items = [
{
id: 1,
name: 'Taro',
pets: ['🐱', '🐶'],
answers: { 'Q1' => '⭕️', 'Q2' => '❌', 'Q3' => '⭕️' }
},
{
id: 2,
name: 'Hanako',
pets: ['🐇', '🐢', '🐿', '🦒'],
answers: { 'Q1' => '⭕️', 'Q2' => '⭕️', 'Q3' => '❌' }
}
]
## To use Rails `find_each` method, write:
# items = Item.enum_for(:find_each)
## or
# items = Enumerator.new { |y| Item.find_each { |item| y << item } }
array = []
writer.write(items, to: array)
# array
# => [["ID", "Name", "Pet 1", "Pet 2", "Pet 3", "Q1", "Q2", "Q3"], ["1", "Taro", "🐱", "🐶", "", "⭕️", "❌", "⭕️"], ["2", "Hanako", "🐇", "🐢", "🐿", "⭕️", "⭕️", "❌"]]
Write the items converted by the schema to file as CSV:
File.open('sample.csv', 'w') do |f|
writer.write(items, to: CSV.new(f))
end
Write the items converted by the schema to stream as CSV with Rails:
# response.headers['X-Accel-Buffering'] = 'no' # Required if Nginx is used for reverse proxy
response.headers['Cache-Control'] = 'no-cache'
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = 'attachment; filename="sample.csv"'
response.headers['Last-Modified'] = Time.now.ctime.to_s # Required if Rack >= 2.2.0
response_body = Enumerator.new do |y|
# y << "\uFEFF" # BOM (Prevent garbled characters for Excel)
writer.write(items, to: CSV.new(y))
end
You can also convert CSV character code:
File.open('sample.csv', 'w') do |f|
writer.write(items, to: CSV.new(f)) do |row|
row.map { |val| val.to_s.encode('Shift_JIS', invalid: :replace, undef: :replace) }
end
end
You can also use TableStructure::CSV::Writer
instead:
writer = TableStructure::CSV::Writer.new(schema)
File.open('sample.csv', 'w') do |f|
writer.write(items, to: f, bom: true)
end
If you want to convert the item to row as Hash instead of Array, specify row_type: :hash
.
To use this option, define :key
on column(s)
.
Define a schema:
class SampleTableSchema
include TableStructure::Schema
column name: 'ID',
key: :id,
value: ->(row, *) { row[:id] }
column name: 'Name',
key: :name,
value: ->(row, *) { row[:name] }
column name: ['Pet 1', 'Pet 2', 'Pet 3'],
key: %i[pet1 pet2 pet3],
value: ->(row, *) { row[:pets] }
columns do |table|
table[:questions].map do |question|
{
name: question[:id],
key: question[:id].downcase.to_sym,
value: ->(row, *) { row[:answers][question[:id]] }
}
end
end
end
Initialize a iterator with the schema:
context = {
questions: [
{ id: 'Q1', text: 'Do you like sushi?' },
{ id: 'Q2', text: 'Do you like yakiniku?' },
{ id: 'Q3', text: 'Do you like ramen?' }
]
}
schema = SampleTableSchema.new(context: context)
iterator = TableStructure::Iterator.new(schema, header: false, row_type: :hash)
Enumerate the items converted by the schema:
items = [
{
id: 1,
name: 'Taro',
pets: ['🐱', '🐶'],
answers: { 'Q1' => '⭕️', 'Q2' => '❌', 'Q3' => '⭕️' }
},
{
id: 2,
name: 'Hanako',
pets: ['🐇', '🐢', '🐿', '🦒'],
answers: { 'Q1' => '⭕️', 'Q2' => '⭕️', 'Q3' => '❌' }
}
]
enum = iterator.iterate(items)
## Enumerator methods is available
enum.each do |item|
# ...
end
enum.map(&:itself)
# => [{:id=>1, :name=>"Taro", :pet1=>"🐱", :pet2=>"🐶", :pet3=>nil, :q1=>"⭕️", :q2=>"❌", :q3=>"⭕️"}, {:id=>2, :name=>"Hanako", :pet1=>"🐇", :pet2=>"🐢", :pet3=>"🐿", :q1=>"⭕️", :q2=>"⭕️", :q3=>"❌"}]
enum.lazy.select { |item| item[:q1] == '⭕️' }.take(1).force
# => [{:id=>1, :name=>"Taro", :pet1=>"🐱", :pet2=>"🐶", :pet3=>nil, :q1=>"⭕️", :q2=>"❌", :q3=>"⭕️"}]
Initialize a table with the schema and render the table:
<% TableStructure::Table.new(schema, row_type: :hash) do |table| %>
<table>
<thead>
<tr>
<% table.header.each do |key, value| %>
<th class="<%= key %>"><%= value %></th>
<% end %>
</tr>
</thead>
<tbody>
<% table.body(@items).each do |row| %>
<tr>
<% row.each do |key, value| %>
<td class="<%= key %>"><%= value %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<% end %>
https://github.com/jsmmr/ruby_table_structure_sample
You can add definitions when initializing the schema.
class UserTableSchema
include TableStructure::Schema
column name: 'ID',
value: ->(row, *) { row[:id] }
column name: 'Name',
value: ->(row, *) { row[:name] }
end
schema = UserTableSchema.new do
column_builder :to_s do |val|
val.to_s
end
end
You can also omit columns by using :omitted
.
class UserTableSchema
include TableStructure::Schema
column name: 'ID',
value: ->(row, *) { row[:id] }
column name: 'Name',
value: ->(row, *) { row[:name] }
column name: 'Secret',
value: ->(row, *) { row[:secret] },
omitted: ->(table) { !table[:admin] }
end
context = { admin: true }
schema = UserTableSchema.new(context: context)
You can also omit columns by using :nil_definitions_ignored
option.
If this option is set to true
and column(s)
difinition returns nil
, the difinition is ignored.
class SampleTableSchema
include TableStructure::Schema
column name: 'ID',
value: ->(row, *) { row[:id] }
column name: 'Name',
value: ->(row, *) { row[:name] }
columns do |table|
if table[:pet_num].positive?
{
name: (1..table[:pet_num]).map { |num| "Pet #{num}" },
value: ->(row, *) { row[:pets] }
}
end
end
end
context = { pet_num: 0 }
schema = SampleTableSchema.new(context: context, nil_definitions_ignored: true)
You can also use context_builder
to change the context object that the lambda
receives.
class SampleTableSchema
include TableStructure::Schema
TableContext = Struct.new(:questions, keyword_init: true)
RowContext = Struct.new(:id, :name, :pets, :answers, keyword_init: true) do
def increase_pets
pets + pets
end
end
context_builder :table do |context|
TableContext.new(**context)
end
context_builder :row do |context|
RowContext.new(**context)
end
column name: 'ID',
value: ->(row, *) { row.id }
column name: 'Name',
value: ->(row, *) { row.name }
column name: ['Pet 1', 'Pet 2', 'Pet 3'],
value: ->(row, *) { row.increase_pets }
columns do |table|
table.questions.map do |question|
{
name: question[:id],
value: ->(row, *) { row.answers[question[:id]] }
}
end
end
end
You can also nest the schemas.
If you nest the schemas and use row_type: :hash
, :key
must be unique in the schemas.
You can also use :key_prefix
or :key_suffix
option to keep uniqueness of the keys.
class UserTableSchema
include TableStructure::Schema
column name: 'ID',
key: :id,
value: ->(row, *) { row[:id] }
column name: 'Name',
key: :name,
value: ->(row, *) { row[:name] }
end
class SampleTableSchema
include TableStructure::Schema
columns UserTableSchema
columns do |table|
UserTableSchema.new(context: table, name_prefix: 'Friend ', key_prefix: 'friend_') do
context_builder :row do |context|
context[:friend]
end
end
end
end
items = [
{
id: 1,
name: 'Taro',
friend: {
id: 2,
name: 'Hanako'
}
}
]
schema = SampleTableSchema.new(context: {})
TableStructure::Iterator.new(schema, row_type: :hash).iterate(items)
You can also concatenate or merge the schema classes. Both create a schema class, with a few differences.
+
- Similar to nesting the schemas.
column_builder
works only to columns in the schema that they was defined.
- Similar to nesting the schemas.
merge
- If there are some definitions of
column_builder
with the same name in the schemas to be merged, the one in the schema that is merged last will work to all columns.
- If there are some definitions of
class UserTableSchema
include TableStructure::Schema
column name: 'ID',
value: ->(row, *) { row[:id] }
column name: 'Name',
value: ->(row, *) { row[:name] }
end
class PetTableSchema
include TableStructure::Schema
column name: ['Pet 1', 'Pet 2', 'Pet 3'],
value: ->(row, *) { row[:pets] }
column_builder :same_name do |val|
"pet: #{val}"
end
end
class QuestionTableSchema
include TableStructure::Schema
columns do |table|
table[:questions].map do |question|
{
name: question[:id],
value: ->(row, *) { row[:answers][question[:id]] }
}
end
end
column_builder :same_name do |val|
"question: #{val}"
end
end
context = {
questions: [
{ id: 'Q1', text: 'Do you like sushi?' },
{ id: 'Q2', text: 'Do you like yakiniku?' },
{ id: 'Q3', text: 'Do you like ramen?' }
]
}
concatenated_schema = (UserTableSchema + PetTableSchema + QuestionTableSchema).new(context: context)
merged_schema = UserTableSchema.merge(PetTableSchema, QuestionTableSchema).new(context: context)
Bug reports and pull requests are welcome on GitHub at https://github.com/jsmmr/ruby_table_structure.
The gem is available as open source under the terms of the MIT License.