BareFrame-First Design¶
Summary¶
BareFrame is the general-purpose DataFrame for all data operations.
FlameFrame is a validation wrapper that adds a schema.
The design separates concerns:
BareFrame— read, transform, write dataFlameFrame— validate data against a schema (viaFlameFrame.from())
This separation keeps the codebase maintainable and the API predictable.
Philosophy: Separate Transformation from Validation¶
In standard Google Apps Script and many libraries, data operations and validation are mixed:
// Standard approach: validation mixed with transformation
function processData(rows) {
const cleaned = [];
for (const row of rows) {
// validate each field
if (typeof row.age !== 'number' || row.age < 0) {
// handle error... but how? skip? throw? log?
continue;
}
// transform
cleaned.push({ ...row, ageGroup: row.age > 65 ? 'senior' : 'other' });
}
return cleaned;
}
Problems:
- Validation and transformation are entangled
- Error handling is unclear
- Hard to reuse validation rules
- Hard to understand what the function does at a glance
The Solution: Two Classes, Two Responsibilities¶
BareFrame does transformation.
FlameFrame does validation.
They're separate.
// Step 1: Read and transform (BareFrame)
let df = gaslamp.BareFrame.fromSheet(sheet);
df = df.filter(row => row.get('age') > 0); // clean junk
df = df.withColumn('ageGroup', row => row.get('age') > 65 ? 'senior' : 'other');
// Step 2: Validate (FlameFrame)
const schema = {
name: gaslamp.FlameGuards.isString,
age: gaslamp.FlameGuards.isNumber,
ageGroup: gaslamp.FlameWright.enumOf(['senior', 'other']),
};
const { passed, failed } = gaslamp.FlameFrame.from(df, schema);
// Step 3: Handle results
if (failed.length > 0) {
console.log('Invalid rows:', failed.toString());
}
return passed; // only validated data
Each step is clear:
- Transform: Use BareFrame operations
- Validate: Use FlameFrame as a gate
- Handle: Use
passedfor next steps,failedfor error logging
How It Works: The Workflow¶
BareFrame.fromSheet(sheet)
↓
[df: raw data]
↓
df.filter(...).select(...) [BareFrame operations]
↓
[df: transformed, unvalidated]
↓
FlameFrame.from(df, schema) [validation gate]
↓
{passed: FlameFrame<T>, failed: BareFrame}
↓
passed.filter(...).toSheet() [continue with validated data]
Key insight: FlameFrame is not for transformation. It's a gate that splits data into valid and invalid.
Design Rules¶
Rule 1: BareFrame is the Workhorse¶
All reading, transforming, and writing happens on BareFrame:
// Read
const df = gaslamp.BareFrame.fromSheet(sheet);
const df2 = gaslamp.BareFrame.fromColumns({ name: ["Alice"], age: [25] });
// Transform
const filtered = df.filter(row => row.get('age') > 18);
const selected = filtered.select(['name', 'age']);
const grouped = selected.groupBy('department');
// Write
df.toSheet(outputSheet);
Rule 2: FlameFrame Has One Job¶
FlameFrame validates data against a schema. It has one factory method: FlameFrame.from().
// This is the ONLY way to create a FlameFrame
const { passed, failed } = gaslamp.FlameFrame.from(df, schema);
// NOT: FlameFrame.fromSheet(), FlameFrame.fromColumns(), etc.
// The user builds a BareFrame first, then validates
Rule 3: FlameFrame Does Not Add Transformation Methods¶
FlameFrame does not have filter(), select(), withColumn(), etc. methods of its own.
Why?
- Prevents confusion about what class does what
- Forces explicit separation: transform, then validate
- Keeps the API small and predictable
However, FlameFrame extends BareFrame, so passed inherits all transformation methods:
// This is OK (passed is a FlameFrame, which extends BareFrame)
const additional = passed.filter(row => row.get('department') === 'Sales');
// But we don't add NEW transformation methods to the FlameFrame class itself
Rule 4: Choose the Right Frame for Each Step¶
// Before validation: use df (BareFrame)
const df = gaslamp.BareFrame.fromSheet(sheet);
const cleaned = df.filter(row => row.get('name') !== ''); // clean junk
// After validation: use passed (FlameFrame)
const { passed, failed } = gaslamp.FlameFrame.from(cleaned, schema);
const final = passed.filter(row => row.get('status') === 'active'); // safe to use
// failed is always BareFrame (no schema)
if (failed.length > 0) {
console.log(failed.toString()); // inspect errors
}
Why This Matters for Code Maintenance¶
For Users¶
The separation is clear:
- "I'm transforming data" → use
BareFramemethods - "I'm checking if data is valid" → use
FlameFrame.from()
No ambiguity, no mixing concerns.
For Library Maintainers¶
The separation is constraining:
BareFrameis the place for all transformation featuresFlameFrameis the place for validation features- No overlap, no confusion about where to add new methods
Example: If you want to add a new operation like unfold():
// Where does it go?
// Answer: BareFrame, because it's a transformation
// How does validation use it?
// Answer: Transform first, then validate the result
const df = gaslamp.BareFrame.fromSheet(sheet);
const unfolded = df.unfold(...); // BareFrame method
const { passed } = gaslamp.FlameFrame.from(unfolded, schema); // then validate
This pattern works forever. No special cases.
Historical Context¶
This design evolved:
- v0.62.0: Introduced BareFrame-first principle. Removed factory shortcuts from FlameFrame.
- v0.64.0: Made FlameFrame extend BareFrame so
passedinherits all transformation methods. - v0.65.0: Clarified that
failedis alwaysBareFrame(neverFlameFrame<never>).
These changes enforced the separation while keeping the API ergonomic.
Related¶
- FlameFrame and Validation — How to use FlameFrame.from()
- Expressions and Predicates — Writing complex filter conditions for BareFrame
- Data Orientation and Conversion — Internal data shapes that BareFrame works with