Skip to content

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 data
  • FlameFrame — validate data against a schema (via FlameFrame.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:

JavaScript
// 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.

JavaScript
// 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:

  1. Transform: Use BareFrame operations
  2. Validate: Use FlameFrame as a gate
  3. Handle: Use passed for next steps, failed for error logging

How It Works: The Workflow

Text Only
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:

JavaScript
// 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().

JavaScript
// 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:

JavaScript
// 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

JavaScript
// 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 BareFrame methods
  • "I'm checking if data is valid" → use FlameFrame.from()

No ambiguity, no mixing concerns.

For Library Maintainers

The separation is constraining:

  • BareFrame is the place for all transformation features
  • FlameFrame is 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():

JavaScript
// 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 passed inherits all transformation methods.
  • v0.65.0: Clarified that failed is always BareFrame (never FlameFrame<never>).

These changes enforced the separation while keeping the API ergonomic.