Skip to content

Architecture

This document describes the internal architecture of ZipReport Go.

Overview

┌─────────────────────────────────────────────────────────────────┐
│                          pkg/api                                 │
│                   ZipReport  │  MIMEReport                       │
└───────────────────────────────────────────────────────────────────┘
        ┌───────────────────────┼───────────────────────┐
        │                       │                       │
        ▼                       ▼                       ▼
┌───────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  pkg/report   │     │  pkg/template   │     │  pkg/processor  │
│  ReportFile   │     │   MiyaRender    │     │  ServerBackend  │
│  ReportJob    │     │   Filters       │     │  MIMEProcessor  │
└───────────────┘     └─────────────────┘     └─────────────────┘
        │                       │
        └───────────┬───────────┘
            ┌───────────────┐
            │ pkg/fileutils │
            │  ZipFs        │
            │  DiskFs       │
            └───────────────┘

Package Descriptions

pkg/api

High-level API providing simple interfaces for common use cases.

  • ZipReport - PDF generation via zipreport-server
  • MIMEReport - MIME email generation
  • BaseReport - Shared functionality

The API layer orchestrates template rendering and processing:

func (b *BaseReport) Render(job, data, wrapper) *JobResult {
    // 1. Create renderer
    renderer := template.NewMiyaRender(job.GetReport(), opts...)

    // 2. Render template
    renderer.Render(data)

    // 3. Process with backend
    return b.processor.Process(job)
}

pkg/report

Core types for report definition and configuration.

ReportFile

Represents a loaded report template. Wraps a filesystem (ZipFs or DiskFs) with a parsed manifest.

type ReportFile struct {
    fs       FsInterface  // ZipFs or DiskFs
    manifest *Manifest    // Parsed manifest.json
}

Key methods:

  • Load(path) - Auto-detect and load from file or directory
  • LoadFile(filename) - Load from .zpt file
  • LoadDir(dirPath) - Load from directory
  • GetBytes(name) - Read file contents
  • Add(name, data, overwrite) - Add/update file
  • Exists(name) - Check file existence

ReportJob

Configuration for a rendering job:

type ReportJob struct {
    report        *ReportFile
    pageSize      PageSize     // A4, Letter, etc.
    margins       MarginStyle  // standard, none, minimum
    customMargins *Margins     // Custom mm values
    landscape     bool
    settlingTime  int          // ms
    jsTimeout     int          // seconds
    jobTimeout    int          // seconds
    useJSEvent    bool
}

Builder

Functions for creating .zpt files:

  • Build(srcDir, destFile, opts) - Build and save to file
  • BuildZipFs(srcDir, opts) - Build to in-memory ZipFs

pkg/template

Template rendering using the miya engine.

MiyaRender

Wraps miya for report rendering:

type MiyaRender struct {
    report  *report.ReportFile
    env     *miya.Environment
    options *RenderOptions
    wrapper EnvironmentWrapper
}

Features:

  • Jinja2-compatible syntax via miya
  • Custom file loader for ReportFile
  • Built-in image filters (png, gif, jpg, svg)
  • Parameter validation
  • Default data from data.json

EnvironmentWrapper

Interface for customizing the miya environment:

type EnvironmentWrapper interface {
    Configure(env *miya.Environment)
}

Implementations:

  • FuncEnvironmentWrapper - Function-based wrapper
  • ChainedWrapper - Combines multiple wrappers

Filters

Built-in filters for dynamic content:

Filter Description
png Generate PNG image
gif Generate GIF image
jpg/jpeg Generate JPEG image
svg Generate SVG image
json Convert to JSON string

Image filters:

  1. Call the generator function with data
  2. Add generated bytes to the report
  3. Return an <img> tag as safe HTML

pkg/processor

Backend implementations for different output types.

Interface

type RenderBackend interface {
    Render(zptData []byte, options map[string]interface{}) ([]byte, error)
}

type Processor interface {
    Process(job *report.ReportJob) *report.JobResult
}

ServerBackend

Renders PDFs via zipreport-server HTTP API:

  1. Export ReportFile to ZIP bytes
  2. Build multipart form with ZIP and options
  3. POST to /v2/render
  4. Return PDF bytes

Configuration options:

  • API version (default: 2)
  • Timeout (default: 5 minutes)
  • SSL verification
  • Custom HTTP client

MIMEProcessor

Generates MIME messages for email:

  1. Render template to HTML
  2. Parse HTML for images
  3. Embed images as MIME parts
  4. Build multipart/related message

pkg/fileutils

File system abstractions for uniform file access.

Interface

type FsInterface interface {
    Get(name string) (io.ReadCloser, error)
    GetBytes(name string) ([]byte, error)
    Add(name string, content []byte, overwrite bool) error
    Mkdir(name string) error
    Exists(name string) bool
    IsDir(name string) bool
    List(path string) ([]string, error)
    ListFiles(path string) ([]string, error)
    ListDirs(path string) ([]string, error)
    Remove(name string) error
}

ZipFs

In-memory ZIP filesystem:

  • NewZipFs() - Create empty
  • NewZipFsFromFile(filename) - Load from file
  • NewZipFsFromBytes(data) - Load from bytes
  • Save() - Export to bytes

Used for .zpt files and report export.

DiskFs

Read-only disk filesystem:

  • NewDiskFs(basePath) - Create from directory
  • BasePath() - Get base directory

Used for directory-based templates during development.

PathCache

Trie-based structure for efficient path lookups:

type PathCache struct {
    mu   sync.RWMutex
    root *pathNode
}

type pathNode struct {
    children map[string]*pathNode
    isFile   bool
    isDir    bool
}

Used internally by ZipFs for fast file/directory existence checks.

Data Flow

PDF Generation

1. Load Template
   report.Load("template.zpt")
   Creates ReportFile with ZipFs

2. Create Job
   api.NewZipReport(url, apiKey)
   client.CreateJob(zpt)
   Creates ReportJob with settings

3. Render Template
   template.NewMiyaRender(zpt)
   renderer.Render(data)
   Executes template, writes report.html

4. Process
   processor.ZipReportProcessor.Process(job)
   Exports to ZIP, sends to server

5. Return Result
   JobResult{Report: pdfBytes, Success: true}

MIME Generation

1. Load & Render Template
   (same as PDF steps 1-3)

2. Process with MIME
   processor.MIMEProcessor.Process(job)
   - Parse HTML
   - Find images
   - Embed as MIME parts
   - Build message

3. Return Result
   JobResult{Report: mimeBytes, Success: true}

Design Decisions

File System Abstraction

Both ZipFs and DiskFs implement the same interface, allowing:

  • Transparent handling of .zpt files and directories
  • Easy development with directories, deployment with .zpt
  • Testability with in-memory filesystems

Separation of Concerns

  • api - User-facing, simple interface
  • report - Data structures, no I/O
  • template - Rendering logic
  • processor - Output generation
  • fileutils - Storage abstraction

Template Engine Choice

Uses miya for Jinja2 compatibility:

  • Familiar syntax for Python users
  • Full template inheritance support
  • Extensible filter system
  • Good performance

Backend Abstraction

The RenderBackend interface allows:

  • Easy testing with mocks
  • Future local rendering support
  • Custom backends for special cases

Extension Points

Custom Filters

wrapper := template.FuncEnvironmentWrapper(func(env *miya.Environment) {
    env.AddFilter("myfilter", myFilterFunc)
})

Custom Backends

type MyBackend struct{}

func (b *MyBackend) Render(zpt []byte, opts map[string]interface{}) ([]byte, error) {
    // Custom rendering logic
}

client := api.NewZipReportWithBackend(&MyBackend{})

Custom File Systems

type MyFs struct {
    // Custom storage
}

// Implement all FsInterface methods:
func (f *MyFs) Get(name string) (io.ReadCloser, error) { ... }
func (f *MyFs) GetBytes(name string) ([]byte, error) { ... }
func (f *MyFs) Add(name string, content []byte, overwrite bool) error { ... }
func (f *MyFs) Mkdir(name string) error { ... }
func (f *MyFs) Exists(name string) bool { ... }
func (f *MyFs) IsDir(name string) bool { ... }
func (f *MyFs) List(path string) ([]string, error) { ... }
func (f *MyFs) ListFiles(path string) ([]string, error) { ... }
func (f *MyFs) ListDirs(path string) ([]string, error) { ... }
func (f *MyFs) Remove(name string) error { ... }