Skip to content

FormatStyle Design

sat edited this page Dec 3, 2025 · 1 revision

FormatStyle Design

Overview

FormatStyle (formerly FileFormat) controls how configuration blocks are formatted and merged in dot2net. It uses a clear two-phase approach:

  1. Format Phase: Individual config blocks are formatted with prefixes/suffixes
  2. Merge Phase: Multiple formatted blocks are combined together

This design ensures that formatting is applied exactly once, avoiding the double-formatting issues that existed in earlier versions.

FormatStyle Structure

type FormatStyle struct {
    Name string

    // Format Phase (applied to individual config blocks)
    FormatLinePrefix     string  // Prefix added to each line
    FormatLineSuffix     string  // Suffix added to each line
    FormatLineSeparator  string  // Separator between lines
    FormatBlockPrefix    string  // Prefix added to entire block
    FormatBlockSuffix    string  // Suffix added to entire block

    // Merge Phase (applied when combining multiple blocks)
    MergeBlockSeparator  string  // Separator between blocks
    MergeResultPrefix    string  // Prefix added to entire merged result
    MergeResultSuffix    string  // Suffix added to entire merged result

    // Legacy fields (v0.6.x compatibility, removed in v0.7.0)
    LinePrefix     string  // Falls back to FormatLinePrefix
    LineSuffix     string  // Falls back to FormatLineSuffix
    LineSeparator  string  // Falls back to FormatLineSeparator
    BlockPrefix    string  // Falls back to FormatBlockPrefix
    BlockSuffix    string  // Falls back to FormatBlockSuffix
    BlockSeparator string  // Falls back to MergeBlockSeparator
}

Two-Phase Processing

Format Phase

The Format Phase processes a single config block (template output) and applies line-level and block-level formatting:

Input: ["router ospf", "network 10.0.1.0/24 area 0"]

Step 1 - formatConfigLines():
  ├─ Split into lines: ["router ospf", "network 10.0.1.0/24 area 0"]
  ├─ Apply to each line: FormatLinePrefix + line + FormatLineSuffix
  └─ Join with: FormatLineSeparator

  Example (format_lineprefix="  ", format_linesuffix=";", format_lineseparator="\n"):
    "  router ospf;\n  network 10.0.1.0/24 area 0;"

Step 2 - formatSingleConfigBlock():
  ├─ Take formatted lines from Step 1
  ├─ Wrap with: FormatBlockPrefix + content + FormatBlockSuffix

  Example (format_blockprefix="BEGIN\n", format_blocksuffix="\nEND"):
    "BEGIN\n  router ospf;\n  network 10.0.1.0/24 area 0;\nEND"

Output: Fully formatted single config block

Merge Phase

The Merge Phase combines multiple already-formatted blocks:

Input: ["Block1", "Block2", "Block3"] (already formatted in Format Phase)

Step 1 - mergeConfigBlocks():
  ├─ Join blocks with: MergeBlockSeparator

  Example (merge_blockseparator="\n---\n"):
    "Block1\n---\nBlock2\n---\nBlock3"

Step 2 - Wrap result (optional):
  ├─ If MergeResultPrefix or MergeResultSuffix is set:
  └─ Wrap with: MergeResultPrefix + merged + MergeResultSuffix

  Example (merge_resultprefix="# Config Start\n", merge_resultsuffix="\n# Config End"):
    "# Config Start\nBlock1\n---\nBlock2\n---\nBlock3\n# Config End"

Output: Fully merged and wrapped result

Key Design Principle

Format is applied ONCE - Config blocks are formatted during the Format Phase and stored. The Merge Phase only combines these pre-formatted blocks; it never re-applies Format Phase formatting.

This prevents double-formatting issues where the same formatting rules were incorrectly applied multiple times.

When Each Phase is Used

Format Phase

Always used when processing any ConfigTemplate:

  • Interface templates
  • Node templates
  • Connection templates
  • Network templates

Every template output goes through Format Phase formatting before being stored or used.

Merge Phase

Only used when combining multiple config blocks:

  1. Sorter Templates (style: sort):

    - file: "frr.conf"
      style: sort
      sort_group: "frr_config"
      format: frr_format

    Merges all blocks collected in sort_group into a single file.

  2. blocks.before / blocks.after:

    - file: "startup.sh"
      scope: node
      class: [router]
      blocks:
        before: ["interfaces_startup"]  # Merged before node template
        after: ["cleanup_commands"]     # Merged after node template
      format: shell_format

    Child configs are formatted individually, then merged.

Real-World Examples

FRR vtysh Command Format

Used to generate vtysh command-line invocations:

format:
  - name: frr_vtysh
    format_lineseparator: "\" -c \""
    format_blockprefix: "vtysh -c \"conf t\" -c \""
    format_blocksuffix: "\""

Template output:

["router ospf", "network 10.0.1.0/24 area 0"]

Result:

vtysh -c "conf t" -c "router ospf" -c "network 10.0.1.0/24 area 0"

Containerlab YAML List Format

Used for exec command lists:

format:
  - name: clab_cmd
    format_lineprefix: "      - "
    merge_blockseparator: "\n"

Template output (multiple blocks):

Block 1: ["/usr/lib/frr/frr start"]
Block 2: ["vtysh -c 'conf t' -c 'router ospf'"]

Result:

      - /usr/lib/frr/frr start
      - vtysh -c 'conf t' -c 'router ospf'

TiNET spec.yaml Format

Used for node command definitions:

format:
  - name: tn_spec_cmd
    format_lineprefix: "      - cmd: "
    merge_blockseparator: "\n"

Template output:

["ip addr add 10.0.1.1/24 dev net0", "ip link set net0 up"]

Result:

      - cmd: ip addr add 10.0.1.1/24 dev net0
      - cmd: ip link set net0 up

Complex Example with All Fields

format:
  - name: complex_format
    # Format Phase
    format_lineprefix: "    "           # 4-space indent
    format_linesuffix: ";"              # Semicolon after each line
    format_lineseparator: "\n"          # Newline between lines
    format_blockprefix: "  config {\n"  # Block starts with "config {"
    format_blocksuffix: "\n  }"         # Block ends with "}"
    # Merge Phase
    merge_blockseparator: "\n\n"        # Blank line between blocks
    merge_resultprefix: "# Generated Configuration\n"
    merge_resultsuffix: "\n# End Configuration"

Processing example:

Input Block 1: ["option1 value1", "option2 value2"]
Input Block 2: ["option3 value3"]

Format Phase (Block 1):
  config {
      option1 value1;
      option2 value2;
  }

Format Phase (Block 2):
  config {
      option3 value3;
  }

Merge Phase:
# Generated Configuration
  config {
      option1 value1;
      option2 value2;
  }

  config {
      option3 value3;
  }
# End Configuration

Implementation Details

Format Phase Functions

formatConfigLines(lines []string, fs *FormatStyle) string

  • Location: pkg/model/format.go
  • Applies line-level formatting
  • Returns a single string with all lines formatted and joined

formatSingleConfigBlock(content string, fs *FormatStyle) string

  • Location: pkg/model/format.go
  • Applies block-level formatting
  • Wraps the content with block prefix/suffix

Merge Phase Functions

mergeConfigBlocks(blocks []string, fs *FormatStyle) string

  • Location: pkg/model/format.go
  • Joins multiple pre-formatted blocks
  • Optionally wraps with result prefix/suffix
  • Important: Does NOT re-apply Format Phase formatting

SorterConfigBlocks Integration

The SorterConfigBlocks structure stores formatted blocks with priority:

type ConfigBlock struct {
    Block    string  // Already formatted in Format Phase
    Priority int     // For ordering within group
}

func (scb *SorterConfigBlocks) getConfigBlocks(ns types.NameSpacer, group string) []string {
    // Sort by priority, then return pre-formatted blocks
    // These blocks are then passed to mergeConfigBlocks()
}

Backward Compatibility

v0.6.x Compatibility

Legacy field names are supported through fallback mechanism:

func (fs *FormatStyle) getFormatLinePrefix() string {
    if fs.FormatLinePrefix != "" {
        return fs.FormatLinePrefix
    }
    return fs.LinePrefix  // Legacy fallback
}

All modules provide both new and legacy fields:

&types.FormatStyle{
    Name: "frr_vtysh",
    // New fields (v0.6.0+)
    FormatLineSeparator: "\" -c \"",
    // Legacy fields (v0.6.x compatibility)
    LineSeparator: "\" -c \"",
}

v0.7.0 Breaking Changes

Legacy fields will be removed:

  • lineprefix, linesuffix, lineseparator
  • blockprefix, blocksuffix, blockseparator

Users must migrate to format_* and merge_* prefixed fields.

Debugging Tips

Enable Verbose Output

dot2net build -c input.yaml input.dot --verbose

Look for these messages:

  • "Format Phase - formatConfigLines(...)" - Line formatting applied
  • "Format Phase - formatSingleConfigBlock(...)" - Block formatting applied
  • "Merge Phase - mergeConfigBlocks(...)" - Multiple blocks merged

Common Issues

1. Double formatting: Pre-v0.6.0 issue, now fixed

  • Symptom: Format prefix/suffix applied twice
  • Solution: Upgrade to v0.6.0+

2. Missing block separator:

  • Symptom: Blocks merged without separator
  • Solution: Set merge_blockseparator appropriately

3. Unexpected line breaks:

  • Symptom: Extra newlines in output
  • Solution: Check format_lineseparator and merge_blockseparator

Related Documentation