FakeDataDSL Usage Guide

Complete guide on how to use FakeDataDSL: DSL syntax, types, formulas, unique constraints, and options.

Table of Contents


Basic DSL Syntax

Schema Definition

A schema starts with a name followed by a colon:

SchemaName:
  field1: type1
  field2: type2

Field Definition

Basic field syntax:

field_name: type

Example:

User:
  id: uuid
  name: name
  email: email
  age: number(18..65)

Comments

Use # for comments:

User:
  # Primary identifier
  id: uuid
  name: name  # User's full name
  email: email

Types

Basic Types

FakeDataDSL provides many built-in types:

User:
  # Identifiers
  id: uuid
  user_id: id_sequence

  # Text
  name: name
  first_name: first_name
  last_name: last_name
  email: email
  bio: text(50..500)
  username: username

  # Numbers
  age: number(18..100)
  score: number(min: 0, max: 100)
  price: number(10.0..99.99)

  # Boolean
  active: boolean
  verified: boolean(true: 80%)  # 80% chance of true

  # Dates
  created_at: timestamp
  birth_date: date
  last_login: past_date(days: 30)

  # Location
  country: country
  city: city
  address: address
  postal_code: postal_code

  # Internet
  website: url
  ip_address: ip
  mac_address: mac_address
  user_agent: user_agent

Type Arguments

Many types accept arguments to customize generation:

Number with Range

age: number(18..65)              # Range
price: number(min: 10, max: 100) # Min/max
score: number(0..100)            # Integer range

Text with Length

title: text(10..50)    # 10-50 characters
description: text(100) # Exactly 100 characters
bio: text(min: 50, max: 500)

Enum with Weights

status: enum(active, inactive, pending)
priority: enum(low: 50%, medium: 30%, high: 20%)
role: enum(user: 80%, admin: 10%, moderator: 10%)

Array Types

tags: array(text, 3..10)           # Array of text, 3-10 items
scores: array(number(0..100), 5)   # Array of numbers, exactly 5 items
items: array(Item, 1..5)            # Array of nested schemas

Date Types

created_at: timestamp
birth_date: date
last_week: past_date(days: 7)
next_month: future_date(days: 30)
between: date_between(start: "2024-01-01", end: "2024-12-31")

All Available Types

See Type Reference for a complete list of all 80+ types including:

  • Personal: first_name, last_name, full_name, gender, age, username, email, phone, national_id
  • Location: country, city, state, address, latitude, longitude, timezone, postal_code
  • Commerce: product_name, product_category, sku, price, discount, tax_rate
  • Finance: credit_card, bank_name, account_number, iban, money, currency, transaction_amount
  • Date & Time: date, time, datetime, timestamp, past_date, future_date, date_between
  • Internet/IT: uuid, ip, ipv6, mac_address, domain, url, user_agent
  • Crypto: hash, bitcoin_address, ethereum_address, private_key
  • Health: blood_type, bmi, heart_rate, medical_code
  • Travel: airport_code, airline, flight_number, seat_number
  • Advanced: sequence, default_value, formula, template, regex, json_array

Formula

The formula type computes values from other fields using mathematical expressions.

Basic Formula

Order:
  quantity: number(1..10)
  unit_price: number(10..100)
  total: formula(quantity * unit_price)

Generated Example:

{
  "quantity" => 5,
  "unit_price" => 50,
  "total" => 250
}

Complex Formulas

Order:
  quantity: number(1..10)
  unit_price: number(10..100)
  subtotal: formula(quantity * unit_price)
  tax_rate: number(0.05..0.15)
  tax: formula(subtotal * tax_rate)
  discount: number(0..20)
  discount_amount: formula(subtotal * discount / 100)
  total: formula(subtotal + tax - discount_amount)

Generated Example:

{
  "quantity" => 3,
  "unit_price" => 75,
  "subtotal" => 225,
  "tax_rate" => 0.08,
  "tax" => 18,
  "discount" => 10,
  "discount_amount" => 22.5,
  "total" => 220.5
}

Formula Operations

Formulas support:

  • Arithmetic: +, -, *, /, %
  • Parentheses: (quantity * price) + tax
  • Field References: Use field names directly in expressions
  • Numbers: total * 0.08, price + 10

Example:

Product:
  base_price: number(10..100)
  markup: number(1.1..1.5)
  final_price: formula(base_price * markup)
  shipping: number(5..15)
  total: formula(final_price + shipping)

Formula Limitations

  • Only works with numeric field values
  • Field must be generated before the formula field
  • Returns 0 if expression fails to evaluate
  • No string concatenation (use template type instead)

Unique Constraints

The unique modifier ensures generated values are unique within a batch.

Basic Unique

User:
  id: uuid unique
  email: email unique
  username: username unique

Unique with Types

Product:
  sku: sku unique
  code: text(10) unique
  serial_number: sequence(start: 1000) unique

Unique with Enums

Order:
  status: enum(pending, processing, completed) unique
  # Each generated order will have a different status

How Unique Works

  1. Per-Batch: Uniqueness is enforced within a single generate_many call
  2. Retries: Generator retries up to max_unique_retries times (default: 100)
  3. Error: Raises UniquenessError if unique value cannot be generated

Example:

registry = FakeDataDSL::Registry.new
registry.load_string(<<~DSL)
  User:
    id: uuid unique
    email: email unique
DSL

schema = registry.schema("User")

# Generate 10 unique users
users = schema.generate_many(10, seed: 42)
# All users will have different IDs and emails

Unique Configuration

Configure max retries:

FakeDataDSL.configure do |config|
  config.max_unique_retries = 200  # Default: 100
end

Options and Modifiers

Nullable Fields

Add ? to make a field nullable (can be nil):

User:
  middle_name: name?      # Type is nullable
  nickname: text?          # May be nil
  manager: User?          # Reference can be nil

Optional Fields

Use optional modifier to omit fields sometimes:

User:
  phone: phone optional      # May be omitted from output
  address: address optional # Sometimes not included

Difference:

  • Nullable (?): Field is always included, but value may be nil
  • Optional: Field may be completely omitted from output

Conditional Fields

Use if to conditionally include fields:

User:
  is_admin: boolean
  admin_note: text if is_admin        # Only if is_admin is true
  employee_id: text if is_employee    # Only if is_employee is true

Note: The condition field must be defined before the conditional field.

Type Options

Many types support options:

Number Options

age: number(18..65)              # Range
price: number(min: 10, max: 100) # Named arguments
score: number(0..100, precision: 2)  # With precision

Text Options

title: text(10..50)                    # Length range
description: text(min: 100, max: 500)  # Named arguments
code: text(10, prefix: "CODE-")        # With prefix

# Language support (English/Arabic)
english_text: text(50, language: :en)   # English (default)
arabic_text: text(50, language: :ar)    # Arabic
arabic_text: text(50, lang: :arabic)    # Also accepts :arabic

Enum Options

status: enum(active, inactive)
priority: enum(low: 50%, medium: 30%, high: 20%)  # With weights
role: enum(user: 80%, admin: 10%, moderator: 10%)  # Percentages

Array Options

tags: array(text, 3..10)              # Element type and size
items: array(Item, min: 1, max: 5)     # Named arguments
scores: array(number(0..100), 5)       # Fixed size

Date Options

created_at: timestamp
last_week: past_date(days: 7)
next_month: future_date(days: 30)
between: date_between(start: "2024-01-01", end: "2024-12-31")

Sequence Options

id: sequence(start: 1, step: 1)
code: sequence(start: "A001", step: 1, format: "ID-%03d")

Template Options

full_name: template("{{first_name}} {{last_name}}")
email: template("{{username}}@example.com")

Combining Modifiers

You can combine multiple modifiers:

User:
  id: uuid unique                    # Unique constraint
  email: email unique optional       # Unique and optional
  nickname: text? optional           # Nullable and optional
  admin_note: text? if is_admin      # Nullable and conditional

Deterministic Generation (Same Data Every Time)

Use the seed parameter to generate the same data every time. This is essential for:

  • Testing - Reproducible test data
  • Debugging - Same data to debug issues
  • Documentation - Consistent examples
  • CI/CD - Predictable test results

Basic Usage

require 'fake_data_dsl'

registry = FakeDataDSL::Registry.new
registry.load_string(<<~DSL)
  User:
    id: uuid
    name: name
    email: email
    age: number(18..65)
DSL

schema = registry.schema("User")

# Generate with seed - same data every time
user1 = schema.generate(seed: 42)
user2 = schema.generate(seed: 42)

# user1 and user2 will be IDENTICAL
puts user1 == user2  # => true

Generate Multiple Records

# Generate 10 records with same seed
users1 = schema.generate_many(10, seed: 12345)
users2 = schema.generate_many(10, seed: 12345)

# All records will be identical
users1.each_with_index do |user, i|
  puts user == users2[i]  # => true for all
end

Different Seeds = Different Data

# Different seeds produce different data
user1 = schema.generate(seed: 42)
user2 = schema.generate(seed: 43)

puts user1 == user2  # => false (different data)

Streaming with Seed

# Streaming also supports seeds for determinism
streamer = schema.generate_many_stream(count: 100, seed: 999)

# All records will be deterministic
streamer.each do |record|
  # Process record
end

Complete Example

require 'fake_data_dsl'

# Define schema
registry = FakeDataDSL::Registry.new
registry.load_string(<<~DSL)
  Product:
    id: uuid
    name: product_name
    price: number(10..100)
    category: product_category
    in_stock: boolean
DSL

schema = registry.schema("Product")

# Generate same product every time
product = schema.generate(seed: 42)
puts product
# => {
#   "id" => "550e8400-e29b-41d4-a716-446655440000",
#   "name" => "Ergonomic Granite Chair",
#   "price" => 45.67,
#   "category" => "Electronics",
#   "in_stock" => true
# }

# Generate again with same seed - IDENTICAL
product2 = schema.generate(seed: 42)
puts product == product2  # => true

# Different seed - different data
product3 = schema.generate(seed: 99)
puts product == product3  # => false

Seed Best Practices

  1. Use Fixed Seeds for Tests

    # In your test
    user = schema.generate(seed: 12345)
    expect(user["age"]).to be_between(18, 65)
    
  2. Use Different Seeds for Variety

    # Generate varied test data
    (1..100).each do |seed|
     user = schema.generate(seed: seed)
     # Each user will be different
    end
    
  3. Store Seeds for Reproducibility

    # Save seed with generated data
    seed = 42
    user = schema.generate(seed: seed)
    user["_seed"] = seed  # Store for later reproduction
    
  4. Seed Per Schema

    # Each schema can have different seeds
    user = user_schema.generate(seed: 100)
    order = order_schema.generate(seed: 200)
    

Determinism Guarantees

Guaranteed Deterministic:

  • Same seed + same schema = identical output
  • Works across multiple runs
  • Works across different machines
  • Works with generate, generate_many, and generate_many_stream

⚠️ Not Deterministic:

  • Without seed (uses random seed each time)
  • Different Ruby versions (may vary)
  • Different Faker versions (may vary)
  • Schema changes (different schema = different output)

Example: Testing with Seeds

# spec/user_spec.rb
RSpec.describe "User generation" do
  let(:schema) do
    registry = FakeDataDSL::Registry.new
    registry.load_string(<<~DSL)
      User:
        id: uuid
        name: name
        email: email
        age: number(18..65)
    DSL
    registry.schema("User")
  end

  it "generates consistent data with seed" do
    user1 = schema.generate(seed: 42)
    user2 = schema.generate(seed: 42)

    expect(user1).to eq(user2)
    expect(user1["age"]).to be_between(18, 65)
  end

  it "generates different data with different seeds" do
    user1 = schema.generate(seed: 1)
    user2 = schema.generate(seed: 2)

    expect(user1).not_to eq(user2)
  end
end

Configuration

You can also set a default seed globally:

FakeDataDSL.configure do |config|
  # Note: There's no global seed setting, always pass seed explicitly
  # This ensures explicit control over determinism
end

# Always pass seed explicitly for clarity
schema.generate(seed: 42)

Fixed Values (Same Value Every Time)

Sometimes you need specific fields to always have the same value every time you generate data, regardless of seed. This is useful for:

  • Timestamps - Fixed created_at for all records
  • Constants - Fixed status, category, or identifier
  • Test Data - Known values for testing

Use the default_value type to set a fixed value:

User:
  id: uuid unique
  username: username unique
  email: email unique
  first_name: first_name
  last_name: last_name
  full_name: template("{{first_name}} {{last_name}}")
  age: number(18..100)
  gender: gender
  phone: phone? optional
  address: address? optional
  is_admin: boolean(true: 10%)
  admin_level: enum(none: 90%, moderator: 8%, admin: 2%) if is_admin
  created_at: default_value(value: "2024-01-15 10:30:00 UTC")
  last_login: past_date(days: 30)? optional

For Timestamps:

User:
  # Fixed timestamp (same for all records)
  created_at: default_value(value: "2024-01-15T10:30:00Z")

  # Or use Time object format
  updated_at: default_value(value: "2024-01-15 10:30:00 UTC")

  # Or Unix timestamp
  timestamp: default_value(value: 1705315800)

For Other Types:

Product:
  # Fixed string
  status: default_value(value: "active")

  # Fixed number
  version: default_value(value: 1)

  # Fixed boolean
  enabled: default_value(value: true)

  # Fixed array
  tags: default_value(value: ["featured", "new"])

Method 2: Using Overrides

Override specific fields when generating:

require 'fake_data_dsl'

registry = FakeDataDSL::Registry.new
registry.load_string(<<~DSL)
  User:
    id: uuid unique
    username: username unique
    email: email unique
    created_at: timestamp
DSL

schema = registry.schema("User")

# Generate with fixed created_at
fixed_time = Time.parse("2024-01-15 10:30:00 UTC")

user1 = schema.generate(
  seed: 42,
  overrides: { "created_at" => fixed_time }
)

user2 = schema.generate(
  seed: 43,
  overrides: { "created_at" => fixed_time }
)

# Both have same created_at
puts user1["created_at"] == user2["created_at"]  # => true

For Multiple Records:

fixed_time = Time.parse("2024-01-15 10:30:00 UTC")

users = schema.generate_many(
  10,
  seed: 42,
  overrides: { "created_at" => fixed_time }
)

# All users have the same created_at
users.each do |user|
  puts user["created_at"] == fixed_time  # => true
end

Method 3: Combining Seed + Overrides

Use seed for most fields, but override specific ones:

User:
  id: uuid unique
  username: username unique
  email: email unique
  first_name: first_name
  last_name: last_name
  full_name: template("{{first_name}} {{last_name}}")
  age: number(18..100)
  gender: gender
  phone: phone? optional
  address: address? optional
  is_admin: boolean(true: 10%)
  admin_level: enum(none: 90%, moderator: 8%, admin: 2%) if is_admin
  created_at: timestamp  # Will be overridden
  last_login: past_date(days: 30)? optional
# Generate with seed for randomness, but fixed created_at
fixed_created_at = Time.parse("2024-01-15 10:30:00 UTC")

user = schema.generate(
  seed: 42,
  overrides: { "created_at" => fixed_created_at }
)

# Other fields vary with seed, but created_at is always the same

Complete Example

require 'fake_data_dsl'

registry = FakeDataDSL::Registry.new
registry.load_string(<<~DSL)
  User:
    id: uuid unique
    username: username unique
    email: email unique
    first_name: first_name
    last_name: last_name
    full_name: template("{{first_name}} {{last_name}}")
    age: number(18..100)
    gender: gender
    phone: phone? optional
    address: address? optional
    is_admin: boolean(true: 10%)
    admin_level: enum(none: 90%, moderator: 8%, admin: 2%) if is_admin
    created_at: default_value(value: "2024-01-15 10:30:00 UTC")
    last_login: past_date(days: 30)? optional
DSL

schema = registry.schema("User")

# Generate multiple users - all have same created_at
users = schema.generate_many(10, seed: 42)

users.each do |user|
  puts user["created_at"]  # => "2024-01-15 10:30:00 UTC" (same for all)
  puts user["first_name"]  # => Different names (varies with seed)
end

When to Use Each Method

Use default_value when:

  • ✅ Value should be the same in the DSL definition
  • ✅ Value is a constant that never changes
  • ✅ You want it defined in the schema itself

Use overrides when:

  • ✅ Value needs to be set at generation time
  • ✅ Value might vary per batch but same within batch
  • ✅ You want flexibility to change it programmatically

Tips

  1. Timestamp Format: Use ISO 8601 format for timestamps:

    created_at: default_value(value: "2024-01-15T10:30:00Z")
    
  2. Time Objects: You can also use Time objects in overrides:

    overrides: { "created_at" => Time.parse("2024-01-15 10:30:00 UTC") }
    
  3. Multiple Fixed Fields: Set multiple fixed values:

    overrides: {
     "created_at" => fixed_time,
     "status" => "active",
     "version" => 1
    }
    

Complete Examples

E-commerce Order

Order:
  id: uuid unique
  order_number: sequence(start: 1000) unique
  customer_id: uuid
  items: array(OrderItem, 1..5)
  subtotal: formula(items.map(&:total).sum)
  tax_rate: number(0.05..0.15)
  tax: formula(subtotal * tax_rate)
  shipping: number(5..25)
  total: formula(subtotal + tax + shipping)
  status: enum(pending: 20%, processing: 30%, shipped: 40%, delivered: 10%)
  created_at: timestamp
  shipped_at: timestamp? optional

OrderItem:
  product_id: uuid
  product_name: product_name
  quantity: number(1..10)
  unit_price: number(10..100)
  total: formula(quantity * unit_price)

User Profile

User:
  id: uuid unique
  username: username unique
  email: email unique
  first_name: first_name
  last_name: last_name
  full_name: template("{{first_name}} {{last_name}}")
  age: number(18..100)
  gender: gender
  phone: phone? optional
  address: address? optional
  is_admin: boolean(true: 10%)
  admin_level: enum(none: 90%, moderator: 8%, admin: 2%) if is_admin
  created_at: timestamp
  last_login: past_date(days: 30)? optional

Financial Transaction

Transaction:
  id: uuid unique
  transaction_id: sequence(start: "TXN-0001", format: "TXN-%04d") unique
  from_account: account_number
  to_account: account_number
  amount: transaction_amount(min: 1.0, max: 10000.0)
  currency: currency
  fee: formula(amount * 0.02)
  total: formula(amount + fee)
  status: enum(pending: 10%, processing: 20%, completed: 65%, failed: 5%)
  created_at: timestamp
  completed_at: timestamp? if status == "completed"

Product Catalog

Product:
  id: uuid unique
  sku: sku unique
  name: product_name
  category: product_category
  description: text(50..500)
  price: number(10.0..999.99, precision: 2)
  discount: discount(min: 0, max: 50)
  final_price: formula(price * (1 - discount / 100))
  in_stock: boolean(true: 80%)
  stock_quantity: number(0..1000) if in_stock
  tags: array(text, 3..10)
  created_at: timestamp

Quick Reference

Field Syntax

field_name: type(arguments) modifiers

Common Modifiers

  • unique - Ensure unique values
  • optional - May be omitted
  • ? - Nullable (can be nil)
  • if condition - Conditional field

Common Types

  • uuid - UUID v4
  • name - Full name
  • email - Email address
  • number(range) - Number in range
  • text(length) - Text with length
  • boolean - True/false
  • enum(values) - Enum with values
  • array(type, size) - Array of type
  • formula(expression) - Computed value
  • sequence(start, step) - Sequential values

Type Arguments

  • number(18..65) - Range
  • number(min: 10, max: 100) - Named arguments
  • text(10..50) - Length range
  • enum(low: 50%, high: 50%) - With weights
  • array(text, 3..10) - Element type and size

Next Steps