statecraft 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e98ec25e4cfbb6cec172cd1f54ac9362937c967eff450ccda67d414e2dce6d3
4
- data.tar.gz: e8f112727ef007ad0d6d3e91e67c122298bbf1d403f09aa9e2aeda56e43545da
3
+ metadata.gz: b91fc7675cab350149a2e58679893b18ccdfa779cc84e2eb5a7d28c5591c265c
4
+ data.tar.gz: c751611f88af3a85738877185b68de734578a353818aa130c19206d40b498b82
5
5
  SHA512:
6
- metadata.gz: ed3978c9e0098cfc1ed4a19b933ce2a134e8841385e0e9542c267e43b6095f81c2b97b230a40d04605d0618717b3af289716157c3ab029f85b30886c54a11cb5
7
- data.tar.gz: 626c429ccc91fc9f40401e58ab6142b81544684d57890fb0f51cec79d901e6280d06e2a14afb0400bb6d592faf33e5dbab5ef7c09045e4435b8557dd2162fb73
6
+ metadata.gz: 42c744386532cf91f06374dc3695c4e2ee3e64602fbb428045e3b588c27dc32843959552dea944be5d98834f28036baf1d712da2c25008dba8b16766ca23310b
7
+ data.tar.gz: 86822e6c1fba14b00e6a8e18efc586a7f92f35280490a168e4d96d063a6d3ecd471eac6fe877c512b9972359d1d14f4d344521dc6f0fea56235075df2bae975b
data/README.md CHANGED
@@ -263,8 +263,12 @@ end
263
263
  ```
264
264
 
265
265
  `seen:` rides all four surface forms and the helper verbs. A string token
266
- straight from params is fine — the pipeline normalizes it with `Integer()`,
267
- and garbage raises `ArgumentError` loudly. The honest limits: staleness
266
+ straight from params is fine — the pipeline normalizes it with `Integer()`.
267
+ The field comes back from the browser, so it is hostile input: a token that
268
+ cannot be read at all — a tampered `seen=abc`, an array from `seen[]=1` —
269
+ is refused as `StaleTransition` too, with `expected_version: nil` because
270
+ nothing was compared. A broken or forged form never turns into a 500.
271
+ The honest limits: staleness
268
272
  exists only where a token was given — without `seen:` a version mismatch
269
273
  is the ordinary `TransitionConflict`; the early deterministic check runs
270
274
  only under `lock: true`, where the reload holds the row's real version —
@@ -513,6 +517,11 @@ statecraft's CAS on the state column.
513
517
  Outside Rails, the same five steps work by hand — pair the runbook above
514
518
  with the reference schema in [Outside Rails](#outside-rails).
515
519
 
520
+ Once the conversion has settled, one more `add_column` buys protection
521
+ statesman never had: see [Stale transitions](#stale-transitions-versioning-against-aba)
522
+ — the version column is a constant default, so adding it costs a
523
+ metadata-only migration on PostgreSQL 11+.
524
+
516
525
  ## PII and erasure
517
526
 
518
527
  Metadata is the only place personal data can live — `from_state`, `to_state`
@@ -732,7 +741,7 @@ possibility-times-permission intersection:
732
741
  <!-- readme: preview-pattern -->
733
742
  ```erb
734
743
  <%= form_with url: preview_admin_order_path(@order), method: :post, local: true do %>
735
- <input type="hidden" name="seen" value="<%= @order[:state_version] %>">
744
+ <input type="hidden" name="seen" value="<%= @order.state_version %>">
736
745
  <fieldset>
737
746
  <legend>Metadata for the next action</legend>
738
747
  <label>
@@ -46,18 +46,25 @@ module Statecraft
46
46
  end
47
47
  end
48
48
 
49
- # The refusal of a seen: token: the snapshot the caller acted on is no
50
- # longer the row's version. A subclass of TransitionConflict, so existing
51
- # rescues keep catching it; controllers map it to 409 specifically.
49
+ # The refusal of a seen: token: the snapshot the caller acted on is not
50
+ # the row's version or could not be read at all, which is the same
51
+ # answer with nothing to compare (expected_version stays nil). A subclass
52
+ # of TransitionConflict, so existing rescues keep catching it;
53
+ # controllers map it to 409 specifically.
52
54
  class StaleTransition < TransitionConflict
53
55
  attr_reader :expected_version, :seen
54
56
 
55
57
  def initialize(record:, expected_from:, expected_version:, seen:)
56
58
  @expected_version = expected_version
57
59
  @seen = seen
60
+ reason =
61
+ if expected_version.nil?
62
+ "the token #{seen.inspect} is not a readable version"
63
+ else
64
+ "the caller saw version #{seen.inspect}, but the row has moved on"
65
+ end
58
66
  super(record: record, expected_from: expected_from,
59
- message: "stale transition for #{record.class.name}##{record.id}: " \
60
- "the caller saw version #{seen.inspect}, but the row has moved on")
67
+ message: "stale transition for #{record.class.name}##{record.id}: #{reason}")
61
68
  end
62
69
  end
63
70
 
@@ -22,6 +22,10 @@ module Statecraft
22
22
  column
23
23
  end
24
24
 
25
+ # The token arrives from a form field, so it is hostile input: a
26
+ # tampered or broken form must not escape the gem's own hierarchy.
27
+ # An unreadable token IS an invalid snapshot — the same refusal a
28
+ # stale one gets, with no version to compare against.
25
29
  def normalize_seen(raw_seen)
26
30
  return nil if raw_seen.nil?
27
31
 
@@ -30,7 +34,13 @@ module Statecraft
30
34
  "seen: requires versioning: true on the mounting of #{record.class.name}; " \
31
35
  "add the option or drop the token"
32
36
  end
33
- Integer(raw_seen)
37
+
38
+ begin
39
+ Integer(raw_seen)
40
+ rescue ArgumentError, TypeError
41
+ raise StaleTransition.new(record: record, expected_from: current_state,
42
+ expected_version: nil, seen: raw_seen)
43
+ end
34
44
  end
35
45
 
36
46
  # What the CAS requires the row's version to be: the caller's seen:
@@ -65,10 +65,12 @@ module Statecraft
65
65
 
66
66
  assert_single_column_primary_key
67
67
  @raw_seen = seen
68
- @seen = normalize_seen(seen)
69
68
  metadata = Metadata.normalize(raw_metadata)
70
69
  started_at = Time.current
71
70
  begin
71
+ # Inside the rescue: an unreadable token is a refusal like any
72
+ # other and belongs in the failure telemetry.
73
+ @seen = normalize_seen(seen)
72
74
  edge, event, bypass = edge_resolver.call(current_state)
73
75
  assert_clean_when_locked(edge)
74
76
  frame = open_frame(edge, event)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statecraft
4
- VERSION = "0.7.0"
4
+ VERSION = "0.7.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statecraft
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Pugachev