Module: PaperTrailDiff::Support

Defined in:
lib/paper_trail_diff/support.rb,
sig/generated/paper_trail_diff/support.rbs

Overview

Internal helpers for isolating and serializing values held by result objects.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.ambiguous_message(pair) ⇒ Object

Signature:

  • (Array[untyped]) -> String



103
104
105
106
107
108
109
# File 'lib/paper_trail_diff/support.rb', line 103

def ambiguous_message(pair)
  left, right = pair
  "versions #{left.id.inspect} and #{right.id.inspect} share the timestamp " \
    "#{left.created_at.inspect} and have ids that do not order them, so their " \
    'sequence cannot be recovered; record versions at sub-second precision or ' \
    'with sequential ids'
end

.ambiguous_pair(sorted) ⇒ Object

Signature:

  • (Array[untyped]) -> Array[untyped]?



68
69
70
71
72
73
# File 'lib/paper_trail_diff/support.rb', line 68

def ambiguous_pair(sorted)
  sorted.each_cons(2).find do |left, right|
    left.created_at == right.created_at &&
      !(sequential_id?(left.id) && sequential_id?(right.id))
  end
end

.association_path(parent, name) ⇒ Object

Signature:

  • (String, String) -> String



118
119
120
# File 'lib/paper_trail_diff/support.rb', line 118

def association_path(parent, name)
  parent.empty? ? name : "#{parent}.#{name}"
end

.chronological_sort(versions) ⇒ Object

Ordering falls back to the id whenever timestamps tie, which recovers the real order only while ids increase with insertion. An autoincrement id does; a UUID does not, so a tie between UUID-keyed versions is genuinely unorderable and any timeline built from it would be fiction.

Signature:

  • (Array[untyped]) -> Array[untyped]

Raises:



59
60
61
62
63
64
65
# File 'lib/paper_trail_diff/support.rb', line 59

def chronological_sort(versions)
  sorted = versions.sort_by { |version| chronological_version_key(version) }
  ambiguous = ambiguous_pair(sorted)
  return sorted unless ambiguous

  raise AmbiguousVersionOrderError, ambiguous_message(ambiguous)
end

.chronological_version_key(version) ⇒ Object

Signature:

  • (untyped) -> Array[untyped]



50
51
52
# File 'lib/paper_trail_diff/support.rb', line 50

def chronological_version_key(version)
  [version.created_at, version.id.to_s.rjust(32, '0')]
end

.compare_versions(left, right) ⇒ Object

Signature:

  • (untyped, untyped) -> Integer



112
113
114
115
# File 'lib/paper_trail_diff/support.rb', line 112

def compare_versions(left, right)
  chronological_version_key(left) <=> chronological_version_key(right) ||
    raise(ConfigurationError, 'versions have incomparable timestamps')
end

.immutable_copy(value) ⇒ Object

Signature:

  • (untyped) -> untyped



10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/paper_trail_diff/support.rb', line 10

def immutable_copy(value)
  return value if value.frozen?

  case value
  when Hash
    immutable_hash(value)
  when Array
    value.map { |item| immutable_copy(item) }.freeze
  else
    duplicate_and_freeze(value)
  end
end

.merge_record_groups(existing, incoming) ⇒ Object

Signature:

  • (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]



123
124
125
126
127
128
129
# File 'lib/paper_trail_diff/support.rb', line 123

def merge_record_groups(existing, incoming)
  existing.merge(incoming) do |_name, left, right|
    merged = left.merge(right, ids: (left.fetch(:ids) | right.fetch(:ids)))
    owners = merge_record_owners(left[:owners], right[:owners])
    owners ? merged.merge(owners: owners) : merged
  end
end

.sequential_id?(id) ⇒ Boolean

Signature:

  • (untyped) -> bool

Returns:

  • (Boolean)


98
99
100
# File 'lib/paper_trail_diff/support.rb', line 98

def sequential_id?(id)
  id.is_a?(Integer) || id.to_s.match?(/\A\d+\z/)
end

.serialize(value) ⇒ Object

Signature:

  • (untyped) -> untyped



34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/paper_trail_diff/support.rb', line 34

def serialize(value)
  case value
  when Hash
    serialized = {} #: Hash[untyped, untyped]
    value.each do |key, item|
      serialized[key] = serialize(item)
    end
    serialized
  when Array
    value.map { |item| serialize(item) }
  else
    paper_trail_diff_value?(value) ? value.to_h : value
  end
end

.tied_timestamp_pair(versions) ⇒ Object

Versions sharing a timestamp, whether or not their ids order them. PT-AT indexes association membership per version but resolves it by timestamp, so association state cannot be told apart across such a pair even when the scalar sequence is perfectly recoverable.

Signature:

  • (Array[untyped]) -> Array[untyped]?



80
81
82
# File 'lib/paper_trail_diff/support.rb', line 80

def tied_timestamp_pair(versions)
  versions.each_cons(2).find { |left, right| left.created_at == right.created_at }
end

.versioned?(model_class) ⇒ Boolean

Whether a model records history at all.

PaperTrail defines paper_trail on every ActiveRecord model, so asking whether a class responds to it says nothing -- it is true for models that never called has_paper_trail, and reading history from one of those fails at the version class rather than at the question. Only configured options distinguish the two, which is why this lives in one place: the predicate is easy to write in a form that looks right and always answers true.

Signature:

  • (untyped) -> bool

Returns:

  • (Boolean)


93
94
95
# File 'lib/paper_trail_diff/support.rb', line 93

def versioned?(model_class)
  model_class.respond_to?(:paper_trail_options) && !model_class.paper_trail_options.nil?
end

Instance Method Details

#self?.ambiguous_messageString

: (Array) -> String

Parameters:

  • (Array[untyped])

Returns:

  • (String)


50
# File 'sig/generated/paper_trail_diff/support.rbs', line 50

def self?.ambiguous_message: (Array[untyped]) -> String

#self?.ambiguous_pairArray[untyped]?

: (Array) -> Array?

Parameters:

  • (Array[untyped])

Returns:

  • (Array[untyped], nil)


26
# File 'sig/generated/paper_trail_diff/support.rbs', line 26

def self?.ambiguous_pair: (Array[untyped]) -> Array[untyped]?

#self?.association_pathString

: (String, String) -> String

Parameters:

  • (String)
  • (String)

Returns:

  • (String)


56
# File 'sig/generated/paper_trail_diff/support.rbs', line 56

def self?.association_path: (String, String) -> String

#self?.chronological_sortArray[untyped]

Ordering falls back to the id whenever timestamps tie, which recovers the real order only while ids increase with insertion. An autoincrement id does; a UUID does not, so a tie between UUID-keyed versions is genuinely unorderable and any timeline built from it would be fiction. : (Array) -> Array

Parameters:

  • (Array[untyped])

Returns:

  • (Array[untyped])


23
# File 'sig/generated/paper_trail_diff/support.rbs', line 23

def self?.chronological_sort: (Array[untyped]) -> Array[untyped]

#self?.chronological_version_keyArray[untyped]

: (untyped) -> Array

Parameters:

  • (Object)

Returns:

  • (Array[untyped])


16
# File 'sig/generated/paper_trail_diff/support.rbs', line 16

def self?.chronological_version_key: (untyped) -> Array[untyped]

#self?.compare_versionsInteger

: (untyped, untyped) -> Integer

Parameters:

  • (Object)
  • (Object)

Returns:

  • (Integer)


53
# File 'sig/generated/paper_trail_diff/support.rbs', line 53

def self?.compare_versions: (untyped, untyped) -> Integer

#self?.duplicate_and_freezeObject

: (untyped) -> untyped

Parameters:

  • (Object)

Returns:

  • (Object)


62
# File 'sig/generated/paper_trail_diff/support.rbs', line 62

def self?.duplicate_and_freeze: (untyped) -> untyped

#self?.immutable_copyObject

: (untyped) -> untyped

Parameters:

  • (Object)

Returns:

  • (Object)


7
# File 'sig/generated/paper_trail_diff/support.rbs', line 7

def self?.immutable_copy: (untyped) -> untyped

#self?.immutable_hashHash[untyped, untyped]

: (Hash[untyped, untyped]) -> Hash[untyped, untyped]

Parameters:

  • (Hash[untyped, untyped])

Returns:

  • (Hash[untyped, untyped])


10
# File 'sig/generated/paper_trail_diff/support.rbs', line 10

def self?.immutable_hash: (Hash[untyped, untyped]) -> Hash[untyped, untyped]

#self?.merge_record_groupsHash[String, Hash[Symbol, untyped]]

: (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]

Parameters:

  • (Hash[String, Hash[Symbol, untyped]])
  • (Hash[String, Hash[Symbol, untyped]])

Returns:

  • (Hash[String, Hash[Symbol, untyped]])


59
# File 'sig/generated/paper_trail_diff/support.rbs', line 59

def self?.merge_record_groups: (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]

#self?.merge_record_ownersHash[String, Array[untyped]]?

: (Hash[String, Array[untyped]]?, Hash[String, Array[untyped]]?) -> Hash[String, Array[untyped]]?

Parameters:

  • (Hash[String, Array[untyped]], nil)
  • (Hash[String, Array[untyped]], nil)

Returns:

  • (Hash[String, Array[untyped]], nil)


65
# File 'sig/generated/paper_trail_diff/support.rbs', line 65

def self?.merge_record_owners: (Hash[String, Array[untyped]]?, Hash[String, Array[untyped]]?) -> Hash[String, Array[untyped]]?

#self?.paper_trail_diff_value?Boolean

: (untyped) -> bool

Parameters:

  • (Object)

Returns:

  • (Boolean)


68
# File 'sig/generated/paper_trail_diff/support.rbs', line 68

def self?.paper_trail_diff_value?: (untyped) -> bool

#self?.sequential_id?Boolean

: (untyped) -> bool

Parameters:

  • (Object)

Returns:

  • (Boolean)


47
# File 'sig/generated/paper_trail_diff/support.rbs', line 47

def self?.sequential_id?: (untyped) -> bool

#self?.serializeObject

: (untyped) -> untyped

Parameters:

  • (Object)

Returns:

  • (Object)


13
# File 'sig/generated/paper_trail_diff/support.rbs', line 13

def self?.serialize: (untyped) -> untyped

#self?.tied_timestamp_pairArray[untyped]?

Versions sharing a timestamp, whether or not their ids order them. PT-AT indexes association membership per version but resolves it by timestamp, so association state cannot be told apart across such a pair even when the scalar sequence is perfectly recoverable. : (Array) -> Array?

Parameters:

  • (Array[untyped])

Returns:

  • (Array[untyped], nil)


33
# File 'sig/generated/paper_trail_diff/support.rbs', line 33

def self?.tied_timestamp_pair: (Array[untyped]) -> Array[untyped]?

#self?.versioned?Boolean

Whether a model records history at all.

PaperTrail defines paper_trail on every ActiveRecord model, so asking whether a class responds to it says nothing -- it is true for models that never called has_paper_trail, and reading history from one of those fails at the version class rather than at the question. Only configured options distinguish the two, which is why this lives in one place: the predicate is easy to write in a form that looks right and always answers true. : (untyped) -> bool

Parameters:

  • (Object)

Returns:

  • (Boolean)


44
# File 'sig/generated/paper_trail_diff/support.rbs', line 44

def self?.versioned?: (untyped) -> bool