Module: Clickwrap::Macros
- Defined in:
- lib/clickwrap/macros.rb
Overview
The host-facing macro. The engine extends ActiveRecord::Base with this
module (via ActiveSupport.on_load(:active_record)), so the actor model can
declare:
class User < ApplicationRecord
has_clickwraps
end
That gives the model its evidence proxy — user.clickwraps.agreed_to?(:terms)
and friends — plus the associations, without Clickwrap reaching into the
model for anything else. Same grammar as the rest of the ecosystem:
has_sessions, has_credits, has_api_keys, has_wallets.
Note what the macro deliberately does NOT add: a dependent: :destroy on
the evidence association. Deleting an account must not silently erase the
record of what that person agreed to; the associations nullify the actor
link and leave a stable pseudonymous reference behind, and what happens next
is a retention decision the host makes on purpose.
The macro is a thin forwarder — all behavior lives in Clickwrap::HasClickwraps
so it is discoverable, testable, and include-able directly when a host
prefers that style.
We don't require_relative the concern here even though this file is
required by the spine at gem-load time. The concern lives under
lib/clickwrap/models/concerns/ and is autoloaded by Zeitwerk (the engine
pushes that subtree under the Clickwrap namespace with models and
concerns collapsed). Requiring it here too would double-manage the same
constant and make Zeitwerk raise on its eager-load pass. This is safe
because the macro body only REFERENCES the constant, and it runs when a host
model calls has_clickwraps — long after boot, when the autoloader is
fully wired.
Instance Method Summary collapse
-
#has_clickwrap_evidence(policy:, statement:, actor:, subject:, tenant: nil, represented_party: nil, required_for_new_records: true) ⇒ Object
For a domain row whose existence was authorized by a clickwrap capture — a withdrawal, a signed declaration, a provisioned contract.
- #has_clickwraps ⇒ Object
Instance Method Details
#has_clickwrap_evidence(policy:, statement:, actor:, subject:, tenant: nil, represented_party: nil, required_for_new_records: true) ⇒ Object
For a domain row whose existence was authorized by a clickwrap capture —
a withdrawal, a signed declaration, a provisioned contract. Expects a
clickwrap_event_id column (bin/rails generate clickwrap:link your_table writes the migration) and an explicit evidence contract:
has_clickwrap_evidence policy: :withdrawal_authorization,
statement: :withdrawal,
actor: :user,
subject: :self
It then reads aloud from either end:
capture_clickwrap_and!(:withdrawal_authorization) do |pending_receipt|
withdrawal.clickwrap_event_id = pending_receipt.event_id
withdrawal.save!
end
withdrawal.clickwrap_event # the evidence event behind this row
withdrawal.clickwrap_receipt # its receipt: .verify, .to_canonical_json
No database foreign key, deliberately: evidence and domain rows keep independent retention schedules. The model contract is still strict for every new link: it checks the policy, statement, human actor, subject, tenant, and represented party, and it never lets a link be replaced. Rows that predate the gem may remain nil; new rows require evidence by default.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/clickwrap/macros.rb', line 68 def has_clickwrap_evidence(policy:, statement:, actor:, subject:, tenant: nil, represented_party: nil, required_for_new_records: true) belongs_to :clickwrap_event, class_name: "Clickwrap::Event", optional: true class_attribute :clickwrap_evidence_contract, instance_writer: false self.clickwrap_evidence_contract = { policy: policy.to_s, statement: statement.to_s, actor: actor, subject: subject, tenant: tenant, represented_party: represented_party, required_for_new_records: required_for_new_records == true }.freeze validate :validate_clickwrap_evidence_is_present_for_new_record validate :validate_clickwrap_evidence_link_cannot_be_replaced validate :validate_clickwrap_evidence_matches_this_record define_method(:clickwrap_receipt) do self.class.column_names.include?("clickwrap_event_id") ? clickwrap_event&.receipt : nil end define_method(:validate_clickwrap_evidence_is_present_for_new_record) do # A host may deploy the model macro before the link migration, and old # data migrations may load today's model while replaying a schema from # before Clickwrap existed. The contract becomes strict as soon as the # column exists; before then there is no attribute that could carry the # evidence, so the macro must stay inert instead of crashing deploys. # Class-level `column_names`, never per-row `has_attribute?`: a partial # SELECT must not read as "this row has no evidence contract". return unless self.class.column_names.include?("clickwrap_event_id") contract = self.class.clickwrap_evidence_contract return unless new_record? && contract.fetch(:required_for_new_records) return if clickwrap_event_id.present? errors.add( :clickwrap_event, "must be linked inside the Clickwrap protected-action transaction" ) end define_method(:validate_clickwrap_evidence_link_cannot_be_replaced) do return unless self.class.column_names.include?("clickwrap_event_id") return unless persisted? && will_save_change_to_clickwrap_event_id? previous_id, next_id = clickwrap_event_id_change_to_be_saved return if previous_id.blank? || previous_id == next_id errors.add( :clickwrap_event, "cannot be replaced or removed after it has been linked" ) end define_method(:validate_clickwrap_evidence_matches_this_record) do return unless self.class.column_names.include?("clickwrap_event_id") should_validate = new_record? || will_save_change_to_clickwrap_event_id? return unless should_validate && clickwrap_event_id.present? event = Clickwrap::Event.find_by(id: clickwrap_event_id) unless event errors.add(:clickwrap_event, "does not identify an existing Clickwrap event") next end contract = self.class.clickwrap_evidence_contract expected_policy = Clickwrap.policy!(contract.fetch(:policy)) expected_statement = expected_policy.statement!(contract.fetch(:statement)) mismatches = [] mismatches << "a capture event" unless event.event_type == "capture" mismatches << "policy #{expected_policy.key.inspect}" unless event.policy_key == expected_policy.key statement_event = event.statements.find do |candidate| candidate.statement_key == expected_statement.key && candidate.action == expected_statement.initial_action end mismatches << "statement #{expected_statement.key.inspect}" unless statement_event expected_actor = clickwrap_evidence_contract_value(contract.fetch(:actor), :actor) mismatches << "this record's actor" unless event.actor_reference == Clickwrap::Reference.actor(expected_actor) expected_subject = clickwrap_evidence_contract_value(contract.fetch(:subject), :subject) unless event.subject_key.to_s == Clickwrap::Reference.subject(expected_subject).to_s mismatches << "this record's subject" end expected_tenant = clickwrap_evidence_contract_value(contract.fetch(:tenant), :tenant) unless event.tenant_key.to_s == Clickwrap::Reference.tenant(expected_tenant).to_s mismatches << "this record's tenant" end expected_party = clickwrap_evidence_contract_value( contract.fetch(:represented_party), :represented_party ) unless event.represented_party_reference.to_s == Clickwrap::Reference.represented_party(expected_party).to_s mismatches << "this record's represented party" end next unless mismatches.any? errors.add( :clickwrap_event, "must be #{mismatches.to_sentence}; the supplied event belongs to a different act" ) end define_method(:clickwrap_evidence_contract_value) do |resolver, name| case resolver when :self self when Symbol, String public_send(resolver) else resolver.respond_to?(:call) ? resolver.call(self) : resolver end rescue NoMethodError => error raise DefinitionError, "The `#{name}:` resolver for #{self.class.name}.has_clickwrap_evidence could not " \ "be called: #{error.}" end private :validate_clickwrap_evidence_is_present_for_new_record, :validate_clickwrap_evidence_link_cannot_be_replaced, :validate_clickwrap_evidence_matches_this_record, :clickwrap_evidence_contract_value self end |
#has_clickwraps ⇒ Object
37 38 39 40 |
# File 'lib/clickwrap/macros.rb', line 37 def has_clickwraps include Clickwrap::HasClickwraps unless include?(Clickwrap::HasClickwraps) self end |