Appendix C - 3. PdfModel (sole PDF mutation surface)
The Structure of PdfModel (sole PDF mutation surface) — its shape at a glance:
The diagram shows the shape of the seam: every call site reaches the resource through the one typed model, and the ban-lint guards the direct edge — it fails the build on any call that tries to skip the model and touch the raw library.
flowchart LR
C1[Call site A] --> M
C2[Call site B] --> M
C3[Call site C] --> M
M["Typed model<br/>(sole seam)"] --> R[(Raw format library)]
C1 -. banned direct call .-> R
L{{Ban-lint}} -. fails build .-> C1
Accessible description: three call sites all route through one typed model to reach the raw format library. A dashed red edge marks a call site attempting a direct call to the library; the ban-lint node fails the build on that edge, so the only surviving path to the library runs through the typed model.
Projected from the catalogue entry canonical-models-and-seams / PdfModel (sole PDF mutation surface).
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — Route all reads and writes of a complex file format through one typed model, with raw access to the underlying library banned by a lint, so the structure is compiler-checked and every mutation passes through a surface that encodes the format's invariants (our instance: PdfModel over the canonical PDF library).
Motivation
The raw canonical PDF library is a minefield of silent, invisible-at-the-call-site failures. Forget SetModified() on an indirect object and the write is silently dropped on save. Call tagPointer.AddTag or dict.Put directly and you can corrupt the /StructTreeRoot, the exact class that produced the v172 corruption. There is no single place to enforce these invariants, so scattered raw calls make the same PDF bug class recur at every call site.
Applicability
- A typed model covering the domain surface. Every operation callers need must exist as a typed mutator, or they're forced back to the raw API.
- A ban-lint on the raw API (the counted sensor) plus a migration of all existing call sites.
- A pinned library version. Minor bumps of the canonical PDF library can silently change auto-tagging, so the seam pins it and gates upgrades behind a regression suite.
Structure
The Structure diagram appears at the top of this page.
Sample Code
Two parts make the seam: a typed facade that encodes the invariant callers keep forgetting, and a ban-lint that fails the build on any raw call so no site can slip past the facade. The facade is tiny — its value is that the dropped-write bug is now unrepresentable, because the only public verb marks the object dirty for you.
# --- the typed facade: the one sanctioned mutation surface ---
class DocModel:
"""Sole seam for mutating the format. Every verb encodes an invariant the raw
library makes it easy to forget — here, 'mark dirty or the write is dropped'."""
def __init__(self, raw):
self._raw = raw # the raw library handle stays private — callers never see it
def set_title(self, obj, title: str) -> None:
obj.title = title
obj.mark_dirty() # the invariant the raw API lets you forget, wired in once
# --- the ban-lint: fails the build if any site calls the raw API directly ---
import ast, sys
RAW_CALLS = {"mark_dirty", "add_tag", "put"} # raw verbs only the facade may call
SEAM_MODULE = "docmodel" # the file that *is* the facade
def lint(path: str, source: str) -> list[str]:
if path.endswith(f"{SEAM_MODULE}.py"):
return [] # the facade is allowed to touch the raw API
findings = []
for node in ast.walk(ast.parse(source)):
if isinstance(node, ast.Attribute) and node.attr in RAW_CALLS:
findings.append(f"{path}:{node.lineno}: raw '.{node.attr}(...)' — route through {SEAM_MODULE}")
return findings
if __name__ == "__main__":
hits = [f for p in sys.argv[1:] for f in lint(p, open(p).read())]
print("\n".join(hits))
sys.exit(1 if hits else 0)
Consequences
- The model must cover everything. A missing operation forces either a
noqaescape (a hole) or a model extension (the right fix, but friction). An incomplete seam weakens the ban. - Version lock-in. Pinning the PDF library for tag-tree stability means upgrades are deliberate, gated work.
- Maintenance surface. The typed mutators + the ban-lint are code to keep current as the format needs grow.
Example use within DocAble
PdfModel.Read+ the typedPrimitives/mutators (each wiringSetModified()+ stamp emission).- The raw-PDF-library ban-lints (one on the raw constructors/calls, one on helper leakage).
- The v172
/StructTreeRootcorruption: the defect class this seam eliminates.
Related Patterns
- Counterpart — the raw-PDF-library ban-lint (hard) holds this construction-mode seam in place; without it, "route through PdfModel" is an unenforced convention.
- See also (sibling) — office-models: the same typed-model + ban-lint pattern for the OpenXML formats; the pair is the defect-class consolidation of raw-library corruption across all four document formats.
- See also — canonical-walkers: how traversal over this typed model is done.