change_requests 0.2.0 → 0.2.2

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -0
  3. data/docs/adr/0001-headless-domain-core.md +40 -0
  4. data/docs/adr/0002-mountable-engine-with-isolated-namespace.md +38 -0
  5. data/docs/adr/0003-actor-references-as-triples.md +43 -0
  6. data/docs/adr/0004-uuid-primary-keys.md +35 -0
  7. data/docs/adr/0005-string-states-with-check-constraints.md +43 -0
  8. data/docs/adr/0006-creation-time-immutability.md +39 -0
  9. data/docs/adr/0007-append-only-audit-trail.md +42 -0
  10. data/docs/adr/0008-staged-multi-quorum-schema.md +43 -0
  11. data/docs/adr/0009-host-owned-schema.md +43 -0
  12. data/docs/adr/0010-operations-must-be-declared.md +57 -0
  13. data/docs/adr/0011-system-sentinel-actor.md +36 -0
  14. data/docs/adr/0012-declared-error-taxonomy.md +40 -0
  15. data/docs/adr/0013-json-runtime-pin.md +37 -0
  16. data/docs/adr/0014-executable-architecture-rules.md +45 -0
  17. data/docs/adr/README.md +25 -0
  18. data/lib/change_requests/authorization/callable.rb +30 -0
  19. data/lib/change_requests/authorization/permissions.rb +66 -1
  20. data/lib/change_requests/commands/approve.rb +69 -0
  21. data/lib/change_requests/commands/base.rb +113 -0
  22. data/lib/change_requests/commands/cancel.rb +43 -0
  23. data/lib/change_requests/commands/comment.rb +40 -0
  24. data/lib/change_requests/commands/create.rb +169 -0
  25. data/lib/change_requests/commands/expire.rb +27 -0
  26. data/lib/change_requests/commands/reject.rb +77 -0
  27. data/lib/change_requests/commands/unapprove.rb +49 -0
  28. data/lib/change_requests/configuration.rb +22 -1
  29. data/lib/change_requests/errors.rb +45 -26
  30. data/lib/change_requests/guards/approve.rb +31 -0
  31. data/lib/change_requests/guards/base.rb +201 -0
  32. data/lib/change_requests/guards/cancel.rb +32 -0
  33. data/lib/change_requests/guards/comment.rb +32 -0
  34. data/lib/change_requests/guards/execute.rb +48 -0
  35. data/lib/change_requests/guards/expire.rb +37 -0
  36. data/lib/change_requests/guards/reject.rb +38 -0
  37. data/lib/change_requests/guards/unapprove.rb +41 -0
  38. data/lib/change_requests/models/approval.rb +1 -1
  39. data/lib/change_requests/models/attempt.rb +3 -1
  40. data/lib/change_requests/models/concerns/actor_columns.rb +12 -6
  41. data/lib/change_requests/models/event.rb +3 -1
  42. data/lib/change_requests/models/quorum.rb +7 -2
  43. data/lib/change_requests/models/request.rb +11 -2
  44. data/lib/change_requests/models/stage.rb +10 -3
  45. data/lib/change_requests/operation.rb +6 -2
  46. data/lib/change_requests/operations.rb +14 -4
  47. data/lib/change_requests/translation.rb +3 -1
  48. data/lib/change_requests/version.rb +1 -1
  49. data/lib/change_requests/workflow.rb +3 -1
  50. data/lib/change_requests.rb +9 -3
  51. data/lib/generators/change_requests/install/templates/migration.rb.tt +5 -0
  52. metadata +33 -1
@@ -23,8 +23,11 @@ module ChangeRequests
23
23
  class_methods do
24
24
  # `label: false` for a reference that names who *may* act rather than who did - the
25
25
  # eligibility rows carry no snapshot, because nothing has happened yet to snapshot.
26
+ #
27
+ # `identity: true` also snapshots `config.actor_identity`, which is what lets the gem tell
28
+ # that Admin#7 and User#99 are one human. Null unless the host configured it (§9.4).
26
29
  def actor_reference(prefix, optional: false, allow_system: false, registry: :actor_types,
27
- label: true)
30
+ label: true, identity: false)
28
31
  declare_type_validation(prefix, optional: optional, allow_system: allow_system,
29
32
  registry: registry)
30
33
 
@@ -33,7 +36,7 @@ module ChangeRequests
33
36
  validates :"#{prefix}_label", presence: true if label
34
37
  end
35
38
 
36
- define_actor_accessors(prefix, registry: registry, label: label)
39
+ define_actor_accessors(prefix, registry: registry, label: label, identity: identity)
37
40
  end
38
41
 
39
42
  # QuorumPermission carries a type and nothing else: NULL there means "any registered class".
@@ -57,7 +60,7 @@ module ChangeRequests
57
60
  allow_nil: true
58
61
  end
59
62
 
60
- def define_actor_accessors(prefix, registry:, label:)
63
+ def define_actor_accessors(prefix, registry:, label:, identity:)
61
64
  generated = Module.new do
62
65
  # No `*_id=` override: the column is a string, so ActiveRecord already casts on
63
66
  # assignment. A User with a uuid key and an Admin with a bigint key share it (§5.7).
@@ -66,10 +69,11 @@ module ChangeRequests
66
69
 
67
70
  return nil if type.blank?
68
71
 
69
- triple = { type: type, id: public_send(:"#{prefix}_id") }
70
- triple[:label] = public_send(:"#{prefix}_label") if label
72
+ reference = { type: type, id: public_send(:"#{prefix}_id") }
73
+ reference[:label] = public_send(:"#{prefix}_label") if label
74
+ reference[:identity] = public_send(:"#{prefix}_identity") if identity
71
75
 
72
- triple
76
+ reference
73
77
  end
74
78
 
75
79
  define_method(:"#{prefix}=") do |actor|
@@ -82,6 +86,8 @@ module ChangeRequests
82
86
  public_send(:"#{prefix}_type=", attributes[:type])
83
87
  public_send(:"#{prefix}_id=", attributes[:id])
84
88
  public_send(:"#{prefix}_label=", attributes[:label]) if label
89
+ public_send(:"#{prefix}_identity=", ChangeRequests.config.actor_identity&.call(actor)) \
90
+ if identity && actor
85
91
  end
86
92
  end
87
93
 
@@ -41,6 +41,8 @@ module ChangeRequests
41
41
 
42
42
  # Expiry, the reaper and undeclared-operation cancellation have no actor. The sentinel keeps the
43
43
  # triple not-null so no presenter or export branches on nil.
44
- def system_actor? = actor_type == SYSTEM_ACTOR[:type]
44
+ def system_actor?
45
+ actor_type == SYSTEM_ACTOR[:type]
46
+ end
45
47
  end
46
48
  end
@@ -47,8 +47,13 @@ module ChangeRequests
47
47
  validates :name, format: { with: NAME_FORMAT }, allow_nil: true
48
48
  validates :name, uniqueness: { scope: :change_request_stage_id }, allow_nil: true
49
49
 
50
- def any_match? = permission_match == "any"
51
- def all_match? = permission_match == "all"
50
+ def any_match?
51
+ permission_match == "any"
52
+ end
53
+
54
+ def all_match?
55
+ permission_match == "all"
56
+ end
52
57
 
53
58
  # A nameless quorum has no display text of its own; the stage's label is the honest answer.
54
59
  def label
@@ -23,7 +23,7 @@ module ChangeRequests
23
23
  # so the row stays readable once the actor and the payload's records are gone.
24
24
  readonly_after_create :operation_key, :operation_version, :service, :method_name,
25
25
  :payload, :payload_labels,
26
- :requester_type, :requester_id, :requester_label,
26
+ :requester_type, :requester_id, :requester_label, :requester_identity,
27
27
  :tenant_type, :tenant_id,
28
28
  :max_attempts
29
29
 
@@ -37,7 +37,7 @@ module ChangeRequests
37
37
  has_many :attempts, -> { order(:number) },
38
38
  class_name: "ChangeRequests::Attempt", inverse_of: :change_request
39
39
 
40
- actor_reference :requester
40
+ actor_reference :requester, identity: true
41
41
  actor_reference :executer, optional: true
42
42
  actor_reference :tenant, optional: true, registry: :tenant_types
43
43
 
@@ -56,5 +56,14 @@ module ChangeRequests
56
56
  def current_stage
57
57
  stages.find_by(position: current_stage_position)
58
58
  end
59
+
60
+ # The attempts rows *are* the count; there is no counter column (§19.12). `max_attempts` is
61
+ # readonly after create and `>= 1` by CHECK, so there is no zero case to defend against.
62
+ #
63
+ # It answers "is there an attempt left", not "may this be executed" - Guards::Execute combines
64
+ # it with the status, and M3a's Commands::Execute and M3b's reaper both read it.
65
+ def retryable?
66
+ attempts.count < max_attempts
67
+ end
59
68
  end
60
69
  end
@@ -35,11 +35,18 @@ module ChangeRequests
35
35
  format: { with: NAME_FORMAT },
36
36
  uniqueness: { scope: :change_request_id }
37
37
 
38
- def any_quorum? = satisfied_by == "any_quorum"
39
- def all_quorums? = satisfied_by == "all_quorums"
38
+ def any_quorum?
39
+ satisfied_by == "any_quorum"
40
+ end
41
+
42
+ def all_quorums?
43
+ satisfied_by == "all_quorums"
44
+ end
40
45
 
41
46
  # Still accepting decisions. A closed stage is immutable and a rejected one stopped the request.
42
- def open? = pending? || satisfied?
47
+ def open?
48
+ pending? || satisfied?
49
+ end
43
50
 
44
51
  # Display text, resolved separately from the identifier (§5.9).
45
52
  def label
@@ -25,11 +25,15 @@ module ChangeRequests
25
25
  @workflow = Workflow.new
26
26
  end
27
27
 
28
- def method_name = @method_name&.to_sym
28
+ def method_name
29
+ @method_name&.to_sym
30
+ end
29
31
 
30
32
  # The two defaults below resolve lazily: initializer order is the host's, and an operations file
31
33
  # that loads before the configuration file should still see the host's defaults.
32
- def max_attempts = @max_attempts || ChangeRequests.config.default_max_attempts
34
+ def max_attempts
35
+ @max_attempts || ChangeRequests.config.default_max_attempts
36
+ end
33
37
 
34
38
  def expires_in
35
39
  return @expires_in if @expires_in_set
@@ -25,11 +25,21 @@ module ChangeRequests
25
25
 
26
26
  # nil, not a raise: every guard asks whether the declaration is still live, and §5.11 makes
27
27
  # that a refusal the guard words itself.
28
- def [](key) = @operations[key.to_s]
28
+ def [](key)
29
+ @operations[key.to_s]
30
+ end
31
+
32
+ def keys
33
+ @operations.keys
34
+ end
29
35
 
30
- def keys = @operations.keys
31
- def each(&) = @operations.each_value(&)
32
- def clear = @operations.clear
36
+ def each(&)
37
+ @operations.each_value(&)
38
+ end
39
+
40
+ def clear
41
+ @operations.clear
42
+ end
33
43
 
34
44
  private
35
45
 
@@ -15,6 +15,8 @@ module ChangeRequests
15
15
  I18n.translate(key, default: default)
16
16
  end
17
17
 
18
- def available? = defined?(I18n) ? true : false
18
+ def available?
19
+ defined?(I18n) ? true : false
20
+ end
19
21
  end
20
22
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChangeRequests
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -14,6 +14,8 @@ module ChangeRequests
14
14
  @stages = stages
15
15
  end
16
16
 
17
- def empty? = stages.empty?
17
+ def empty?
18
+ stages.empty?
19
+ end
18
20
  end
19
21
  end
@@ -19,13 +19,19 @@ module ChangeRequests
19
19
  # `unless mod.respond_to?(:table_name_prefix)`, and its version yields `change_requests_stages`.
20
20
  # `Request` is the one model that overrides its table name; it would derive
21
21
  # `change_request_requests`.
22
- def table_name_prefix = "change_request_"
22
+ def table_name_prefix
23
+ "change_request_"
24
+ end
23
25
 
24
- def config = @config ||= Configuration.new
26
+ def config
27
+ @config ||= Configuration.new
28
+ end
25
29
 
26
30
  # The declaration registry (§6.12). Memoised for the same reason as config: initializers
27
31
  # accumulate into it.
28
- def operations = @operations ||= Operations.new
32
+ def operations
33
+ @operations ||= Operations.new
34
+ end
29
35
 
30
36
  # The (type, id, label) triple for a host record, with the label snapshotted through the
31
37
  # registered lambda. Raises before anything is constantized: the registry is the allowlist, and
@@ -21,6 +21,7 @@ class CreateChangeRequests < ActiveRecord::Migration[<%= migration_version %>]
21
21
  t.string :requester_type, null: false
22
22
  t.string :requester_id, null: false
23
23
  t.string :requester_label, null: false
24
+ t.string :requester_identity
24
25
 
25
26
  t.string :executer_type
26
27
  t.string :executer_id
@@ -65,6 +66,10 @@ class CreateChangeRequests < ActiveRecord::Migration[<%= migration_version %>]
65
66
  t.string :status, null: false, default: "pending"
66
67
 
67
68
  t.datetime :satisfied_at, precision: 6
69
+ # The rejection's counterpart to satisfied_at: when the stage was stopped, and therefore when
70
+ # op.cooldown stops being able to give it back. Written from M9b; here from the first
71
+ # migration so no host needs a second one for it (PLAN §5.2, §7.1).
72
+ t.datetime :rejected_at, precision: 6
68
73
  t.datetime :closed_at, precision: 6
69
74
 
70
75
  t.timestamps precision: 6, null: false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: change_requests
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Finger
@@ -310,8 +310,32 @@ files:
310
310
  - Rakefile
311
311
  - config/routes.rb
312
312
  - docs/08_events_and_notifications.md
313
+ - docs/adr/0001-headless-domain-core.md
314
+ - docs/adr/0002-mountable-engine-with-isolated-namespace.md
315
+ - docs/adr/0003-actor-references-as-triples.md
316
+ - docs/adr/0004-uuid-primary-keys.md
317
+ - docs/adr/0005-string-states-with-check-constraints.md
318
+ - docs/adr/0006-creation-time-immutability.md
319
+ - docs/adr/0007-append-only-audit-trail.md
320
+ - docs/adr/0008-staged-multi-quorum-schema.md
321
+ - docs/adr/0009-host-owned-schema.md
322
+ - docs/adr/0010-operations-must-be-declared.md
323
+ - docs/adr/0011-system-sentinel-actor.md
324
+ - docs/adr/0012-declared-error-taxonomy.md
325
+ - docs/adr/0013-json-runtime-pin.md
326
+ - docs/adr/0014-executable-architecture-rules.md
327
+ - docs/adr/README.md
313
328
  - lib/change_requests.rb
329
+ - lib/change_requests/authorization/callable.rb
314
330
  - lib/change_requests/authorization/permissions.rb
331
+ - lib/change_requests/commands/approve.rb
332
+ - lib/change_requests/commands/base.rb
333
+ - lib/change_requests/commands/cancel.rb
334
+ - lib/change_requests/commands/comment.rb
335
+ - lib/change_requests/commands/create.rb
336
+ - lib/change_requests/commands/expire.rb
337
+ - lib/change_requests/commands/reject.rb
338
+ - lib/change_requests/commands/unapprove.rb
315
339
  - lib/change_requests/configuration.rb
316
340
  - lib/change_requests/configuration/actor_type.rb
317
341
  - lib/change_requests/configuration/registered_type.rb
@@ -319,6 +343,14 @@ files:
319
343
  - lib/change_requests/engine.rb
320
344
  - lib/change_requests/errors.rb
321
345
  - lib/change_requests/guards/.keep
346
+ - lib/change_requests/guards/approve.rb
347
+ - lib/change_requests/guards/base.rb
348
+ - lib/change_requests/guards/cancel.rb
349
+ - lib/change_requests/guards/comment.rb
350
+ - lib/change_requests/guards/execute.rb
351
+ - lib/change_requests/guards/expire.rb
352
+ - lib/change_requests/guards/reject.rb
353
+ - lib/change_requests/guards/unapprove.rb
322
354
  - lib/change_requests/models/.keep
323
355
  - lib/change_requests/models/approval.rb
324
356
  - lib/change_requests/models/approval_quorum.rb