Class: Clickwrap::DocumentVersion

Inherits:
ApplicationRecord show all
Defined in:
lib/clickwrap/models/document_version.rb

Overview

An immutable published version of a document.

Once published_at is set, the bytes and their digest are frozen. There is no supported way to edit them: a change is a new version, and the old row stays exactly as it was because a receipt from last year points at it.

content_digest covers the original source bytes. rendered_content_digest covers the representation actually offered when a source format was transformed for display. Keeping both is what lets a receipt distinguish "this Markdown file existed" from "the server offered this rendered HTML" instead of letting one claim borrow the other's credibility.

Constant Summary collapse

STORAGE_BACKENDS =
%w[database active_storage resolver].freeze

Instance Method Summary collapse

Instance Method Details

#content_bytesObject

Reads the bytes for this version, from wherever the storage adapter put them, and verifies them against the recorded digest before returning. Verification is not optional: silently returning bytes that no longer match would turn this method into a way to launder edited content into an export.



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/clickwrap/models/document_version.rb', line 57

def content_bytes
  bytes = read_bytes

  unless Digest.matches?(bytes, "#{content_digest_algorithm}:#{bare_digest(content_digest)}")
    raise DocumentDigestMismatchError,
          "The stored bytes for document version #{self} no longer match the digest " \
          "recorded when it was published. The evidence that references this version " \
          "cannot be reproduced until that is explained. Recorded: #{content_digest}."
  end

  bytes
end

#prefixed_content_digestObject



104
# File 'lib/clickwrap/models/document_version.rb', line 104

def prefixed_content_digest = content_digest

#presentable_at?(moment = Clickwrap.now) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
49
50
# File 'lib/clickwrap/models/document_version.rb', line 46

def presentable_at?(moment = Clickwrap.now)
  published? &&
    (effective_at.nil? || effective_at <= moment) &&
    (retired_at.nil? || retired_at > moment)
end

#published?Boolean

Returns:

  • (Boolean)


43
# File 'lib/clickwrap/models/document_version.rb', line 43

def published? = published_at.present?

#rendered_bytesObject



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/clickwrap/models/document_version.rb', line 77

def rendered_bytes
  return content_bytes if rendered_content.nil?

  unless rendered_content_digest.present? && Digest.matches?(rendered_content, rendered_content_digest)
    raise DocumentDigestMismatchError,
          "The rendered bytes for document version #{self} no longer match the digest " \
          "recorded when they were published."
  end

  rendered_content
end

#retire!(because:, at: Clickwrap.now) ⇒ Object



96
97
98
99
100
101
102
# File 'lib/clickwrap/models/document_version.rb', line 96

def retire!(because:, at: Clickwrap.now)
  raise DocumentVersionConflictError, "Retiring a document version needs a `because:`." if because.to_s.strip.empty?
  raise DocumentVersionConflictError, "Document version #{self} is already retired." if retired?

  update_columns(retired_at: at, retired_reason: because)
  self
end

#retired?Boolean

Returns:

  • (Boolean)


44
# File 'lib/clickwrap/models/document_version.rb', line 44

def retired? = retired_at.present?

#to_sObject



106
# File 'lib/clickwrap/models/document_version.rb', line 106

def to_s = "#{document&.document_key} #{version_label} (#{locale})"

#verify_content_digestObject



70
71
72
73
74
75
# File 'lib/clickwrap/models/document_version.rb', line 70

def verify_content_digest
  content_bytes
  true
rescue DocumentDigestMismatchError, DocumentNotPublishedError
  false
end

#verify_rendered_content_digestObject



89
90
91
92
93
94
# File 'lib/clickwrap/models/document_version.rb', line 89

def verify_rendered_content_digest
  rendered_bytes
  true
rescue DocumentDigestMismatchError, DocumentNotPublishedError
  false
end