Class: ActiveSanction::Diff::Change
- Inherits:
-
Object
- Object
- ActiveSanction::Diff::Change
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/diff/change.rb
Overview
One entity that is on both snapshots and is not the same on each, with the fields that moved.
change.entity # => Entity, as the new list has it
change.previous # => Entity, as the old list had it
change.fields # => [:names, :programs]
change.changes
# => { names: { added: [#<Name "ZAYDAN, Muhammad">], removed: [] },
# programs: { added: ["SDGT"], removed: [] } }
puts change # => ofac_sdn:2674 names +1, programs +1
Why an amendment is not a delisting plus a listing
Governments amend far more records than they publish or withdraw: a passport number is corrected, an alias is added, a program is amended. Reporting one of those as a removal followed by an addition puts a delisting in front of an analyst that never happened -- and a delisting is the entry a compliance team acts on, since it is the one that lets a customer back through the door. So the two snapshots are joined by entity id and only what actually moved is reported, which is what makes id stability a conformance requirement for every adapter (#16) rather than a nicety.
Collections are compared as sets
names, addresses, identifiers, dates_of_birth, nationalities
and programs are compared by membership rather than position: a
publisher that re-emits the same four aliases in a different order has
not amended the record, and a diff that says it has costs somebody a
review. Everything else is a scalar and is reported as from and to.
FIELDS is derived from Entity::MEMBERS rather than written out, so a
field added to the canonical record is compared here without anyone
having to remember to add it. A new collection still has to be named in
COLLECTIONS -- until it is, it is compared whole, which is a coarse
answer rather than a silently missing one.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
-
#changes ⇒ Hash{Symbol => Hash{Symbol => T.untyped}}
readonly
Field to detail, in Entity's member order.
-
#entity ⇒ T.untyped
readonly
The entity as the new snapshot has it.
-
#previous ⇒ T.untyped
readonly
The entity as the old snapshot had it.
Class Method Summary collapse
-
.between(previous, current) ⇒ T.attached_class?
The change between two versions of one entity, or nil when they say the same thing.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
-
#[](field) ⇒ Hash{Symbol => T.untyped}?
What moved in one field, or nil if that field did not.
- #changed?(field) ⇒ Boolean
- #fields ⇒ Array<Symbol>
- #hash ⇒ Integer
-
#id ⇒ String
The id both versions share, which is the whole reason this is one record rather than two.
- #initialize(previous:, entity:, changes:) ⇒ void constructor
- #inspect ⇒ String
-
#summary ⇒ String
One line, for the summary a human reads:.
-
#to_h ⇒ Hash{Symbol => T.untyped}
JSON-ready: every value object is serialized the way the snapshot serializes it, so a consumer that already reads entities can read a change without a second vocabulary.
- #to_s ⇒ String
Constructor Details
#initialize(previous:, entity:, changes:) ⇒ void
117 118 119 120 121 122 |
# File 'lib/active_sanction/diff/change.rb', line 117 def initialize(previous:, entity:, changes:) @previous = T.let(previous, T.untyped) @entity = T.let(entity, T.untyped) @changes = T.let(changes.freeze, T::Hash[Symbol, T::Hash[Symbol, T.untyped]]) freeze end |
Instance Attribute Details
#changes ⇒ Hash{Symbol => Hash{Symbol => T.untyped}} (readonly)
Field to detail, in Entity's member order. A collection field carries
{ added:, removed: } and a scalar { from:, to: }.
86 87 88 |
# File 'lib/active_sanction/diff/change.rb', line 86 def changes @changes end |
#entity ⇒ T.untyped (readonly)
The entity as the new snapshot has it.
77 78 79 |
# File 'lib/active_sanction/diff/change.rb', line 77 def entity @entity end |
#previous ⇒ T.untyped (readonly)
The entity as the old snapshot had it.
81 82 83 |
# File 'lib/active_sanction/diff/change.rb', line 81 def previous @previous end |
Class Method Details
.between(previous, current) ⇒ T.attached_class?
The change between two versions of one entity, or nil when they say the same thing. Nil rather than an empty change: "this record was amended" and "this record was re-published unchanged" are different answers, and only one of them is worth an analyst's time.
93 94 95 96 97 98 99 |
# File 'lib/active_sanction/diff/change.rb', line 93 def self.between(previous, current) changes = FIELDS.each_with_object({}) do |field, found| detail = compare(field, previous.public_send(field), current.public_send(field)) found[field] = detail if detail end changes.empty? ? nil : new(previous: previous, entity: current, changes: changes) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
162 163 164 165 166 |
# File 'lib/active_sanction/diff/change.rb', line 162 def ==(other) return false unless other.instance_of?(self.class) to_h == other.to_h end |
#[](field) ⇒ Hash{Symbol => T.untyped}?
What moved in one field, or nil if that field did not.
137 |
# File 'lib/active_sanction/diff/change.rb', line 137 def [](field) = changes[field.to_sym] |
#changed?(field) ⇒ Boolean
133 |
# File 'lib/active_sanction/diff/change.rb', line 133 def changed?(field) = changes.key?(field.to_sym) |
#fields ⇒ Array<Symbol>
130 |
# File 'lib/active_sanction/diff/change.rb', line 130 def fields = changes.keys |
#hash ⇒ Integer
170 |
# File 'lib/active_sanction/diff/change.rb', line 170 def hash = [self.class, to_h].hash |
#id ⇒ String
The id both versions share, which is the whole reason this is one record rather than two.
127 |
# File 'lib/active_sanction/diff/change.rb', line 127 def id = entity.id |
#inspect ⇒ String
173 |
# File 'lib/active_sanction/diff/change.rb', line 173 def inspect = "#<#{self.class} #{self}>" |
#summary ⇒ String
One line, for the summary a human reads:
ofac_sdn:2674 names +1 -1, programs +1, remarks "..." -> "..."
156 |
# File 'lib/active_sanction/diff/change.rb', line 156 def summary = fields.map { |field| describe(field) }.join(", ") |
#to_h ⇒ Hash{Symbol => T.untyped}
JSON-ready: every value object is serialized the way the snapshot serializes it, so a consumer that already reads entities can read a change without a second vocabulary.
143 144 145 146 147 148 149 150 |
# File 'lib/active_sanction/diff/change.rb', line 143 def to_h { id: id, entity: entity.to_h, previous: previous.to_h, changes: changes.transform_values { |detail| detail.transform_values { |value| serialize(value) } } } end |
#to_s ⇒ String
159 |
# File 'lib/active_sanction/diff/change.rb', line 159 def to_s = "#{id} #{summary}" |