Proforma
Proforma is a Ruby-based document rendering framework with a focus on being:
- Configurable: the virtual document model allows templates to be defined as configuration
- Simple: basic set of controls that limit flexibility but increase simplicity and standardization
- Extendable: different value resolvers and document rendering engines can be plugged in
The basic premise is you input a dataset and a template and Proforma will:
- Evaluate and data-bind each component.
- Render given data and template as a file.
When a component is in the process of being data-bound it will be evaluated against the current data being read. The evaluator serves two purposes:
- Given a record and an expression, find me a raw value (resolving.)
- Given a record and an expression, return me a formatted string-based value (text templating.)
The basic evaluator that comes with this library is Proforma::HashEvaluator. It is limited to:
- Only resolving values for hash objects and cannot handle nesting / recursion / dot notation.
- Only text templating simple expressions: if the expression starts with a
$
then it is noted as being a data-bound value. The value proceeding the$
will be resolved and returned. If it does not begin with this special string then the value following the $ will be resolved and returned.
Other libraries that can be plugged in, such as:
- proforma-extended-evaluator: adds nested dot-notation object value resolution and rich text templating.
- proforma-prawn-renderer: adds PDF rendering.
See the libraries respective README files for more information.
Installation
To install through Rubygems:
gem install install proforma
You can also add this to your Gemfile:
bundle add proforma
Examples
The examples in this library will be based on the default plugins that are contained within package and thus will be as bare bones as they can be. See other plugin library documentation to see how the input and output can be greatly enhanced.
Virtual Document Object Model Introduction
The following is a list of components that can be used for modeling:
- Banner: define image, title, and details
- DataTable: define columns with header, body, and footer contents
- Grouping: one-to-many dataset traversal
- Header: large, bold text
- Pane: define columns and lines (akin to a details section)
- Separator: draw a line
- Spacer: blank space below component
- Text: basic text
Most aspects of the components will be data-bound.
Getting Started: Rendering a List
Let's say we have a list of users:
data = [
{ id: 1, first: 'Matt', last: 'Smith' },
{ id: 2, first: 'Katie', last: 'Rizzo' },
{ id: 3, first: 'Nathan', last: 'Nathanson' }
]
template = {
title: 'User List',
children: [
{
type: 'DataTable',
columns: [
{ header: 'ID Number', body: '$id' },
{ header: 'First Name', body: '$first' },
{ header: 'Last Name', body: '$last' }
]
}
]
}
documents = Proforma.render(data, template)
The documents
variable will now be an array with only one document object:
documents = [
{
contents: "ID Number, First Name, Last Name\n1, Matt, Smith\n...", # condensed
extension: ".txt",
title: "User List"
)
]
The contents
attribute will be the rendered text-based table.
Rendering Records
Let's now say instead of rendering a table we want to render one unique document per record:
data = [
{ id: 1, first: 'Matt', last: 'Smith' },
{ id: 2, first: 'Katie', last: 'Rizzo' },
{ id: 3, first: 'Nathan', last: 'Nathanson' }
]
template = {
title: 'User Details',
split: true, # notice the split directive here.
children: [
{
type: 'Pane',
columns: [
{
lines: [
{ label: 'ID Number', value: '$id' },
{ label: 'First Name', value: '$first' }
]
},
{
lines: [
{ label: 'Last Name', value: '$last' }
]
}
]
}
]
}
documents = Proforma.render(data, template)
The documents
variable will now be an array with three document objects:
documents = [
{
contents: "ID Number: 1\nFirst Name: Matt\nLast Name: Smith\n",
extension: ".txt",
title: "User Details"
},
{
contents: "ID Number: 2\nFirst Name: Katie\nLast Name: Rizzo\n",
extension: ".txt",
title: "User Details"
},
{
contents: "ID Number: 3\nFirst Name: Nathan\nLast Name: Nathanson\n",
extension: ".txt",
title: "User Details"
}
]
Each document will have a contents
attribute that contains its respective rendered text-based pane.
Bringing It All Together
Let's build on our previous user list example and add more data. With this additional data, we will add sub-data tables using grouping:
data = [
{
id: 1,
first: 'Matt',
last: 'Smith',
phone_numbers: [
{ type: 'Mobile', number: '444-333-2222' },
{ type: 'Home', number: '444-333-2222' }
]
},
{
id: 2,
first: 'Katie',
last: 'Rizzo',
phone_numbers: [
{ type: 'Fax', number: '888-777-6666' }
]
},
{
id: 3,
first: 'Nathan',
last: 'Nathanson',
phone_numbers: []
}
]
template = {
title: 'User Report',
children: [
{
type: 'Banner',
title: 'System A',
details: "555 N. Michigan Ave.\nChicago, IL 55555\n555-555-5555 ext. 5132"
},
{ type: 'Header', value: 'User List' },
{ type: 'Separator' },
{ type: 'Spacer' },
{
type: 'DataTable',
columns: [
{ header: 'ID Number', body: '$id' },
{ header: 'First Name', body: '$first' },
{ header: 'Last Name', body: '$last' }
]
},
{ type: 'Spacer' },
{
type: 'Grouping',
children: [
{ type: 'Header', value: 'User Details' },
{ type: 'Separator' },
{ type: 'Spacer' },
{
type: 'Pane',
columns: [
{
lines: [
{ label: 'ID Number', value: '$id' },
{ label: 'First Name', value: '$first' }
]
},
{
lines: [
{ label: 'Last Name', value: '$last' }
]
}
]
},
{
type: 'DataTable',
property: 'phone_numbers',
columns: [
{ header: 'Type', body: '$type' },
{ header: 'Number', body: '$number' }
]
},
{ type: 'Spacer' }
]
}
]
}
documents = Proforma.render(data, template)
The documents
variable will now be an array with only one document object:
documents = [
{
contents: '========================================\nSystem A\n=======Nathan...', # ...
extension: '.txt',
title: 'User Report'
}
]
The contents
attribute will now contain:
- banner
- user summary table
- user details pane (one per user)
- user phone numbers table (one per user)
The full contents can be seen in this fixture file.
Contributing
Development Environment Configuration
Basic steps to take to get this repository compiling:
- Install Ruby (check proforma.gemspec for versions supported)
- Install bundler (gem install bundler)
- Clone the repository (git clone [email protected]:bluemarblepayroll/proforma.git)
- Navigate to the root folder (cd proforma)
- Install dependencies (bundle)
Running Tests
To execute the test suite run:
bundle exec rspec spec --format documentation
Alternatively, you can have Guard watch for changes:
bundle exec guard
Also, do not forget to run Rubocop:
bundle exec rubocop
Publishing
Note: ensure you have proper authorization before trying to publish new versions.
After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
- Merge Pull Request into master
- Update
lib/proforma/version.rb
using semantic versioning - Install dependencies:
bundle
- Update
CHANGELOG.md
with release notes - Commit & push master to remote and ensure CI builds master successfully
- Run
bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the.gem
file to rubygems.org.
Code of Conduct
Everyone interacting in this codebase, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
License
This project is MIT Licensed.