julewire-core 1.0.0 → 1.1.0

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/docs/advanced-configuration.md +2 -1
  4. data/docs/configuration.md +14 -7
  5. data/docs/context-and-propagation.md +26 -4
  6. data/docs/contracts.md +25 -50
  7. data/docs/extensions-and-api.md +87 -88
  8. data/docs/health-schema.md +12 -2
  9. data/docs/internals.md +24 -23
  10. data/docs/outputs-and-lifecycle.md +15 -8
  11. data/docs/records-and-data-policy.md +38 -7
  12. data/lib/julewire/core/cli/doctor.rb +20 -16
  13. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  14. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  15. data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
  16. data/lib/julewire/core/cli/log_formats.rb +9 -9
  17. data/lib/julewire/core/cli/tail.rb +19 -14
  18. data/lib/julewire/core/cli/transcode.rb +4 -5
  19. data/lib/julewire/core/cli.rb +1 -1
  20. data/lib/julewire/core/configuration.rb +1 -1
  21. data/lib/julewire/core/context_store.rb +56 -87
  22. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  23. data/lib/julewire/core/destinations/collection.rb +8 -16
  24. data/lib/julewire/core/destinations/definition.rb +1 -9
  25. data/lib/julewire/core/destinations/destination.rb +16 -66
  26. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  27. data/lib/julewire/core/destinations/registry.rb +1 -1
  28. data/lib/julewire/core/destinations/sink.rb +0 -1
  29. data/lib/julewire/core/destinations/synchronized_output.rb +54 -17
  30. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  31. data/lib/julewire/core/destinations/write_step.rb +5 -5
  32. data/lib/julewire/core/destinations.rb +0 -7
  33. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  34. data/lib/julewire/core/diagnostics/doctor.rb +22 -15
  35. data/lib/julewire/core/diagnostics/failure_snapshot.rb +9 -3
  36. data/lib/julewire/core/diagnostics/health.rb +41 -57
  37. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  38. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  39. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  40. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  41. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  42. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  43. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  44. data/lib/julewire/core/execution/boundary.rb +2 -8
  45. data/lib/julewire/core/execution/handle.rb +14 -17
  46. data/lib/julewire/core/execution/lineage.rb +26 -62
  47. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  48. data/lib/julewire/core/execution/scope.rb +17 -22
  49. data/lib/julewire/core/execution/scope_fields.rb +4 -13
  50. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  51. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  52. data/lib/julewire/core/execution/summary_state.rb +16 -19
  53. data/lib/julewire/core/execution/view.rb +8 -17
  54. data/lib/julewire/core/facade_methods.rb +67 -65
  55. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  56. data/lib/julewire/core/fields/bags.rb +23 -27
  57. data/lib/julewire/core/fields/field_set.rb +52 -16
  58. data/lib/julewire/core/fields/field_stack.rb +69 -109
  59. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  60. data/lib/julewire/core/fields/internal.rb +35 -26
  61. data/lib/julewire/core/fields/lookup.rb +9 -13
  62. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  63. data/lib/julewire/core/fields/stack_set.rb +9 -14
  64. data/lib/julewire/core/fields/static_labels.rb +1 -1
  65. data/lib/julewire/core/integration/configurable.rb +4 -3
  66. data/lib/julewire/core/integration/destination_health.rb +28 -7
  67. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  68. data/lib/julewire/core/integration/facade.rb +12 -15
  69. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  70. data/lib/julewire/core/integration/health.rb +0 -3
  71. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  72. data/lib/julewire/core/integration/protocol.rb +20 -0
  73. data/lib/julewire/core/integration/settings.rb +4 -4
  74. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  75. data/lib/julewire/core/integration/subscription.rb +3 -2
  76. data/lib/julewire/core/integration/values.rb +0 -17
  77. data/lib/julewire/core/local_storage.rb +3 -19
  78. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  79. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  80. data/lib/julewire/core/processing/match.rb +6 -4
  81. data/lib/julewire/core/processing/pipeline.rb +46 -40
  82. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  83. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  84. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  85. data/lib/julewire/core/processing/sampling.rb +1 -3
  86. data/lib/julewire/core/processing.rb +2 -2
  87. data/lib/julewire/core/propagation/carrier.rb +73 -19
  88. data/lib/julewire/core/propagation.rb +18 -4
  89. data/lib/julewire/core/records/build_input.rb +75 -0
  90. data/lib/julewire/core/records/console_formatter.rb +6 -6
  91. data/lib/julewire/core/records/deconstruct.rb +1 -1
  92. data/lib/julewire/core/records/display_message.rb +2 -4
  93. data/lib/julewire/core/records/draft.rb +127 -258
  94. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  95. data/lib/julewire/core/records/metadata.rb +4 -3
  96. data/lib/julewire/core/records/public_projection.rb +2 -2
  97. data/lib/julewire/core/records/raw_input.rb +1 -1
  98. data/lib/julewire/core/records/record.rb +30 -18
  99. data/lib/julewire/core/records/severity.rb +7 -15
  100. data/lib/julewire/core/runtime.rb +87 -72
  101. data/lib/julewire/core/runtime_registry.rb +20 -15
  102. data/lib/julewire/core/runtime_state.rb +5 -3
  103. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  104. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  105. data/lib/julewire/core/serialization/bounded_transform.rb +3 -2
  106. data/lib/julewire/core/serialization/bounded_traversal.rb +72 -83
  107. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  108. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  109. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  110. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  111. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  112. data/lib/julewire/core/serialization/serializer.rb +22 -49
  113. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  114. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  115. data/lib/julewire/core/serialization/truncation_metadata.rb +157 -0
  116. data/lib/julewire/core/serialization/value_copy.rb +183 -109
  117. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  118. data/lib/julewire/core/testing.rb +6 -56
  119. data/lib/julewire/core/validation.rb +2 -3
  120. data/lib/julewire/core/version.rb +1 -1
  121. data/lib/julewire/core.rb +2 -7
  122. metadata +7 -19
  123. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  124. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  125. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  126. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  127. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  128. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  129. data/lib/julewire/core/testing/chaos.rb +0 -80
  130. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  131. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  132. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  133. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  134. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  135. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  136. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  137. data/lib/julewire/core/testing/contracts.rb +0 -24
  138. data/lib/julewire/core/testing/coverage.rb +0 -58
  139. data/lib/julewire/core/testing/test_reports.rb +0 -78
@@ -4,8 +4,8 @@ module Julewire
4
4
  module Core
5
5
  module Fields
6
6
  # @api internal
7
- # Immutable layers keep snapshots stable while each stack tracks only its
8
- # current head and versioned read caches.
7
+ # Immutable layers keep snapshots stable while each stack tracks only the
8
+ # current head and snapshot cache.
9
9
  class FieldStack
10
10
  EMPTY_HASH = {}.freeze
11
11
  private_constant :EMPTY_HASH
@@ -13,15 +13,29 @@ module Julewire
13
13
  class Layer
14
14
  attr_reader :fields, :parent
15
15
 
16
- def initialize(parent, fields, delete_paths: nil, clear_parent_deletes: true)
16
+ class << self
17
+ def fields(parent, fields, clear_parent_deletes: true)
18
+ clear_parent_fields = fields if clear_parent_deletes
19
+ new(
20
+ parent,
21
+ fields,
22
+ delete_paths: nil,
23
+ clear_parent_fields:
24
+ )
25
+ end
26
+
27
+ def delete_paths(parent, paths)
28
+ new(parent, nil, delete_paths: paths, clear_parent_fields: nil)
29
+ end
30
+
31
+ private :new
32
+ end
33
+
34
+ def initialize(parent, fields, delete_paths:, clear_parent_fields:)
17
35
  @parent = parent
18
36
  @fields = fields
19
37
  @delete_paths = delete_paths
20
- @clear_parent_deletes = clear_parent_deletes
21
- @active_delete_paths_computed = false
22
- @active_delete_paths = nil
23
- @snapshot = nil
24
- @value_cache = nil
38
+ @clear_parent_fields = clear_parent_fields
25
39
  end
26
40
 
27
41
  def snapshot
@@ -29,81 +43,54 @@ module Julewire
29
43
  end
30
44
 
31
45
  def value_for(key)
32
- return @value_cache[key] if @value_cache&.key?(key)
46
+ return @value_cache.fetch(key) if @value_cache&.key?(key)
33
47
 
34
48
  value = if delete_paths_for_key?(key)
35
49
  FieldSet.value_for(snapshot, key, default: MISSING)
36
50
  else
37
51
  field_value = FieldSet.value_for(@fields, key, default: MISSING)
38
- field_value.equal?(MISSING) ? parent_value_for(key) : Fields::Internal.frozen_copy(field_value)
52
+ field_value.equal?(MISSING) ? parent_value_for(key) : frozen_field_value(field_value)
39
53
  end
40
54
  (@value_cache ||= {})[key] = value
41
55
  end
42
56
 
43
57
  def active_delete_paths
44
- return @active_delete_paths if @active_delete_paths_computed
58
+ return @active_delete_paths if instance_variable_defined?(:@active_delete_paths)
45
59
 
46
60
  @active_delete_paths = build_active_delete_paths
47
- @active_delete_paths_computed = true
48
- @active_delete_paths
49
61
  end
50
62
 
51
63
  def snapshot_cached?
52
64
  !@snapshot.nil?
53
65
  end
54
66
 
55
- def delete_paths_for_snapshot
56
- @clear_parent_deletes ? @delete_paths : active_delete_paths
67
+ def merge_into(snapshot)
68
+ Internal.merge_owned!(snapshot, FieldSet.deep_dup_owned(@fields))
57
69
  end
58
70
 
59
71
  private
60
72
 
61
73
  def build_snapshot
62
- return build_direct_snapshot unless @parent
63
- return build_parent_snapshot if @parent.snapshot_cached?
64
-
65
- snapshot = source_snapshot_base
66
- source_chain.reverse_each do |source|
67
- FieldSet.merge!(snapshot, source.fields)
68
- paths = source.delete_paths_for_snapshot
69
- Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
70
- end
71
- Fields::Internal.frozen_copy(snapshot)
74
+ sources, base = source_layers_and_base
75
+ snapshot = base ? FieldSet.deep_dup_owned(base.snapshot) : {}
76
+ sources.reverse_each { it.merge_into(snapshot) }
77
+ paths = active_delete_paths
78
+ Internal.apply_delete_paths!(snapshot, paths) if paths
79
+ Internal.frozen_owned_copy(snapshot)
72
80
  end
73
81
 
74
- def build_direct_snapshot
75
- snapshot = FieldSet.merge!({}, @fields)
76
- paths = delete_paths_for_snapshot
77
- Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
78
- Fields::Internal.frozen_copy(snapshot)
82
+ def frozen_field_value(value)
83
+ Internal.frozen_owned_copy(value)
79
84
  end
80
85
 
81
- def build_parent_snapshot
82
- snapshot = FieldSet.merge(@parent.snapshot, @fields)
83
- paths = delete_paths_for_snapshot
84
- Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
85
- Fields::Internal.frozen_copy(snapshot)
86
- end
86
+ def source_layers_and_base(source = self, seen_sources = [], sources = [])
87
+ return [sources, source] if source.nil? || source.snapshot_cached?
87
88
 
88
- def source_snapshot_base
89
- source = source_chain_base
90
- source ? FieldSet.deep_dup(source.snapshot) : {}
91
- end
92
-
93
- def source_chain
94
- sources = []
95
- source = self
96
- until source.nil? || source.snapshot_cached?
97
- sources << source
98
- source = source.parent
99
- end
100
- sources
101
- end
89
+ raise Error, "field stack layer cycle" if seen_sources.include?(source)
102
90
 
103
- def source_chain_base
104
- source = self
105
- source = source.parent until source.nil? || source.snapshot_cached?
106
- source
91
+ seen_sources << source
92
+ sources << source
93
+ source_layers_and_base(source.parent, seen_sources, sources)
107
94
  end
108
95
 
109
96
  def parent_value_for(key)
@@ -118,16 +105,14 @@ module Julewire
118
105
 
119
106
  def build_active_delete_paths
120
107
  paths = @parent&.active_delete_paths
121
- paths = clear_active_delete_paths(paths) if paths && @clear_parent_deletes && !@fields.empty?
108
+ paths = clear_active_delete_paths(paths) if paths
122
109
  paths = append_delete_paths(paths) if @delete_paths
123
- return unless paths
124
-
125
- paths.empty? ? nil : paths
110
+ paths
126
111
  end
127
112
 
128
113
  def clear_active_delete_paths(paths)
129
114
  paths = paths.dup
130
- Fields::Internal.clear_delete_paths!(paths, @fields)
115
+ Internal.clear_delete_paths!(paths, @clear_parent_fields)
131
116
  paths
132
117
  end
133
118
 
@@ -137,51 +122,35 @@ module Julewire
137
122
  end
138
123
  private_constant :Layer
139
124
 
140
- def initialize(fields = {}, delete_paths: false, source: nil)
125
+ def initialize(fields = nil, delete_paths: false, source: nil)
141
126
  @source = source
142
127
  @delete_paths_enabled = delete_paths
143
- @version = 0
144
- @snapshot_version = nil
145
- @snapshot = nil
146
- @value_cache = nil
147
- add(fields) if fields.is_a?(Hash) && !fields.empty?
128
+ add(fields)
148
129
  end
149
130
 
150
131
  def snapshot
151
- return @snapshot if @snapshot_version == @version
132
+ return @snapshot if @snapshot
152
133
 
153
134
  @snapshot = @source ? @source.snapshot : EMPTY_HASH
154
- @snapshot_version = @version
155
- @snapshot
156
135
  end
157
136
 
158
- def fork
137
+ def branch
159
138
  self.class.new(delete_paths: @delete_paths_enabled, source: @source)
160
139
  end
161
140
 
162
141
  def value_for(key, default:)
163
- cache = @value_cache
164
- return cache[key] if cache&.key?(key)
165
-
166
- if key.is_a?(String)
167
- key = Fields::Internal.normalize_key(key)
168
- cache = @value_cache
169
- return cache[key] if cache&.key?(key)
170
- end
171
-
142
+ key = Internal.normalize_key(key)
172
143
  value = source_value_for(key)
173
144
  return default if value.equal?(MISSING)
174
145
 
175
- (@value_cache ||= {})[key] = value
146
+ value
176
147
  end
177
148
 
178
149
  def add(fields = nil, owned: false, **keyword_fields)
179
150
  fields = field_input(fields, keyword_fields, owned: owned)
180
- return unless fields.is_a?(Hash)
181
151
  return if fields.empty?
182
152
 
183
- fields = normalize_owned_keys(fields) if owned
184
- @source = Layer.new(@source, fields, clear_parent_deletes: true)
153
+ @source = Layer.fields(@source, fields)
185
154
  invalidate_snapshot!
186
155
  end
187
156
 
@@ -189,16 +158,14 @@ module Julewire
189
158
  return if path.empty?
190
159
  return unless @delete_paths_enabled
191
160
 
192
- @source = Layer.new(@source, {}, delete_paths: [path], clear_parent_deletes: false)
161
+ @source = Layer.delete_paths(@source, [path])
193
162
  invalidate_snapshot!
194
163
  end
195
164
 
196
165
  def with(fields = nil, owned: false, **keyword_fields, &)
197
166
  fields = field_input(fields, keyword_fields, owned: owned)
198
- return yield unless fields.is_a?(Hash)
199
167
  return yield if fields.empty?
200
168
 
201
- fields = normalize_owned_keys(fields) if owned
202
169
  with_layer(fields, &)
203
170
  end
204
171
 
@@ -207,28 +174,25 @@ module Julewire
207
174
 
208
175
  return yield unless @delete_paths_enabled
209
176
 
210
- with_layer({}, delete_paths: [path], &)
177
+ with_delete_layer([path], &)
211
178
  end
212
179
 
213
180
  private
214
181
 
215
182
  def field_input(fields, keyword_fields, owned:)
216
183
  if owned
217
- return fields if keyword_fields.empty?
218
- return keyword_fields if fields.nil?
219
- return fields.merge(keyword_fields) if fields.is_a?(Hash)
184
+ fields = keyword_fields if fields.nil?
185
+ Serialization::DeepFreeze.validate_symbol_hash(fields)
220
186
 
221
- return keyword_fields
187
+ return fields.merge(keyword_fields)
222
188
  end
223
189
 
224
- return FieldSet.deep_symbolize_keys(fields) if keyword_fields.empty?
225
-
226
190
  FieldSet.coerce(fields, keyword_fields)
227
191
  end
228
192
 
229
- def with_layer(fields, delete_paths: nil)
193
+ def with_layer(fields)
230
194
  previous_source = @source
231
- @source = Layer.new(previous_source, fields, delete_paths: delete_paths, clear_parent_deletes: false)
195
+ @source = Layer.fields(previous_source, fields, clear_parent_deletes: false)
232
196
  invalidate_snapshot!
233
197
  begin
234
198
  yield
@@ -238,30 +202,26 @@ module Julewire
238
202
  end
239
203
  end
240
204
 
241
- def normalize_owned_keys(fields)
242
- return fields unless fields.any? { |key, _value| key.is_a?(String) }
243
-
244
- fields.to_h { |key, value| [Fields::Internal.normalize_key(key), value] }
205
+ def with_delete_layer(delete_paths)
206
+ previous_source = @source
207
+ @source = Layer.delete_paths(previous_source, delete_paths)
208
+ invalidate_snapshot!
209
+ begin
210
+ yield
211
+ ensure
212
+ @source = previous_source
213
+ invalidate_snapshot!
214
+ end
245
215
  end
246
216
 
247
217
  def source_value_for(key)
248
218
  return MISSING unless @source
249
219
 
250
- unless @source.parent
251
- # Single-layer hits avoid Layer's delete-path/cache bookkeeping.
252
- field_value = FieldSet.value_for(@source.fields, key, default: MISSING)
253
- return Fields::Internal.frozen_copy(field_value) unless field_value.equal?(MISSING)
254
- end
255
-
256
- value = @source.value_for(key)
257
- value.equal?(MISSING) ? MISSING : value
220
+ @source.value_for(key)
258
221
  end
259
222
 
260
223
  def invalidate_snapshot!
261
- @version += 1
262
224
  @snapshot = nil
263
- @snapshot_version = nil
264
- @value_cache = nil
265
225
  end
266
226
  end
267
227
  end
@@ -11,12 +11,10 @@ module Julewire
11
11
  return target if normalized_path.empty?
12
12
 
13
13
  deep_delete_path!(target, normalized_path)
14
- target
15
14
  end
16
15
 
17
16
  def apply_delete_paths!(target, paths)
18
17
  paths.each { delete_path!(target, it) }
19
- target
20
18
  end
21
19
 
22
20
  def clear_delete_paths!(paths, fields)
@@ -27,7 +25,7 @@ module Julewire
27
25
  end
28
26
 
29
27
  def normalize_path(path)
30
- Array(path).flatten.filter_map { Internal.normalize_key(it) }
28
+ Array(path).flatten.map { Internal.normalize_key(it) }
31
29
  end
32
30
 
33
31
  private
@@ -37,7 +35,7 @@ module Julewire
37
35
 
38
36
  fields.flat_map do |key, value|
39
37
  path = prefix + [Internal.normalize_key(key)]
40
- nested = value.is_a?(Hash) ? field_paths(value, path) : []
38
+ nested = field_paths(value, path)
41
39
  nested.empty? ? [path] : nested
42
40
  end
43
41
  end
@@ -53,12 +51,11 @@ module Julewire
53
51
  key = path.first
54
52
  if path.one?
55
53
  Internal.delete_key!(target, key)
56
- return
54
+ else
55
+ child = FieldSet.value_for(target, key)
56
+ deep_delete_path!(child, path.drop(1))
57
+ Internal.delete_key!(target, key) if child.is_a?(Hash) && child.empty?
57
58
  end
58
-
59
- child = FieldSet.value_for(target, key)
60
- deep_delete_path!(child, path.drop(1))
61
- Internal.delete_key!(target, key) if child.is_a?(Hash) && child.empty?
62
59
  end
63
60
  end
64
61
  end
@@ -4,13 +4,16 @@ module Julewire
4
4
  module Core
5
5
  module Fields
6
6
  module Internal
7
- EMPTY_ARRAY = [].freeze
8
- EMPTY_HASH = {}.freeze
9
- private_constant :EMPTY_ARRAY, :EMPTY_HASH
7
+ FIELD_KEY_ERROR = "field keys must be String or Symbol"
8
+ RECORD_STRING_KEY_ERROR = "record must not use string keys"
9
+ RECORD_SYMBOL_KEY_ERROR = "record keys must be Symbols"
10
10
 
11
11
  class << self
12
12
  def normalize_key(key)
13
- key.is_a?(String) ? key.to_sym : key
13
+ return key.to_sym if key.is_a?(String)
14
+ return key if key.instance_of?(Symbol)
15
+
16
+ raise TypeError, FIELD_KEY_ERROR
14
17
  end
15
18
 
16
19
  def delete_key!(target, key)
@@ -18,16 +21,19 @@ module Julewire
18
21
  end
19
22
 
20
23
  def frozen_copy(value)
21
- return EMPTY_HASH if value.is_a?(Hash) && value.empty?
22
- return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
23
-
24
- Serialization::ValueCopy.call(value, freeze_values: true)
24
+ frozen_copy_with(value, preserve_truncation_metadata: false)
25
25
  end
26
26
 
27
- def deep_merge(left, right)
28
- deep_merge!(FieldSet.deep_symbolize_keys(left), right)
27
+ def frozen_owned_copy(value)
28
+ frozen_copy_with(value, preserve_truncation_metadata: true)
29
29
  end
30
30
 
31
+ def apply_delete_paths!(target, paths) = Deletion.apply_delete_paths!(target, paths)
32
+
33
+ def clear_delete_paths!(paths, fields) = Deletion.clear_delete_paths!(paths, fields)
34
+
35
+ def normalize_path(path) = Deletion.normalize_path(path)
36
+
31
37
  def deep_merge!(target, fields)
32
38
  merge_values!(target, fields) do |value, existing|
33
39
  if existing.is_a?(Hash) && value.is_a?(Hash)
@@ -39,7 +45,7 @@ module Julewire
39
45
  end
40
46
 
41
47
  def deep_merge_owned!(target, fields)
42
- merge_values!(target, fields) do |value, existing|
48
+ merge_owned_values!(target, fields) do |value, existing|
43
49
  if existing.is_a?(Hash) && value.is_a?(Hash)
44
50
  deep_merge_owned!(existing, value)
45
51
  else
@@ -49,26 +55,19 @@ module Julewire
49
55
  end
50
56
 
51
57
  def merge_owned!(target, fields)
52
- merge_values!(target, fields) { |value, _existing| value }
58
+ merge_owned_values!(target, fields) { |value, _existing| value }
53
59
  end
54
60
 
55
- def frozen_deep_symbolize_keys(value)
56
- return EMPTY_HASH if value.is_a?(Hash) && value.empty?
57
- return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
61
+ private
58
62
 
59
- Serialization::ValueCopy.call(value, freeze_values: true, symbolize_keys: true)
63
+ def frozen_copy_with(value, preserve_truncation_metadata:)
64
+ Serialization::ValueCopy.call(
65
+ value,
66
+ freeze_values: true,
67
+ preserve_truncation_metadata: preserve_truncation_metadata
68
+ )
60
69
  end
61
70
 
62
- def delete_path!(target, path) = Deletion.delete_path!(target, path)
63
-
64
- def apply_delete_paths!(target, paths) = Deletion.apply_delete_paths!(target, paths)
65
-
66
- def clear_delete_paths!(paths, fields) = Deletion.clear_delete_paths!(paths, fields)
67
-
68
- def normalize_path(path) = Deletion.normalize_path(path)
69
-
70
- private
71
-
72
71
  def merge_values!(target, fields)
73
72
  return target unless fields.is_a?(Hash)
74
73
 
@@ -80,6 +79,16 @@ module Julewire
80
79
 
81
80
  target
82
81
  end
82
+
83
+ def merge_owned_values!(target, fields)
84
+ return target unless fields.is_a?(Hash)
85
+
86
+ fields.each do |key, value|
87
+ target[key] = yield value, target[key]
88
+ end
89
+
90
+ target
91
+ end
83
92
  end
84
93
  end
85
94
  end
@@ -8,25 +8,21 @@ module Julewire
8
8
  def value(source, key)
9
9
  return unless source.respond_to?(:[])
10
10
 
11
- direct = source[key]
12
- return direct unless direct.nil?
13
-
14
- alternate_key(source, key)
11
+ source[key]
15
12
  rescue StandardError
16
13
  nil
17
14
  end
18
15
 
19
- def blank?(value)
20
- value.nil? || (value.respond_to?(:empty?) && value.empty?)
16
+ # Decoded wire records have String keys, while in-process records use
17
+ # Symbols. Keep this conversion at presentation boundaries instead of
18
+ # making ordinary core lookups tolerant.
19
+ def wire_value(source, key)
20
+ result = value(source, key)
21
+ result.nil? ? value(source, key.to_s) : result
21
22
  end
22
23
 
23
- private
24
-
25
- def alternate_key(source, key)
26
- case key
27
- when Symbol then source[key.name]
28
- when String then source[key.to_sym]
29
- end
24
+ def blank?(value)
25
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
30
26
  end
31
27
  end
32
28
  end
@@ -34,7 +34,7 @@ module Julewire
34
34
 
35
35
  def to_h = section_hash
36
36
 
37
- def [](key) = nil_if_missing(section_value(key, default: MISSING))
37
+ def [](key) = call_store(:value, key, default: nil)
38
38
 
39
39
  private
40
40
 
@@ -51,10 +51,6 @@ module Julewire
51
51
  yield coerce_fields(fields, keyword_fields)
52
52
  end
53
53
 
54
- def nil_if_missing(value)
55
- value.equal?(MISSING) ? nil : value
56
- end
57
-
58
54
  def add_section(fields)
59
55
  call_store(:add, fields)
60
56
  end
@@ -67,10 +63,6 @@ module Julewire
67
63
  call_store(:hash)
68
64
  end
69
65
 
70
- def section_value(key, default:)
71
- call_store(:value, key, default: default)
72
- end
73
-
74
66
  def call_store(action, ...)
75
67
  @store.public_send(store_method(action), ...)
76
68
  end
@@ -4,13 +4,12 @@ module Julewire
4
4
  module Core
5
5
  module Fields
6
6
  class StackSet
7
- EMPTY_HASH = {}.freeze
8
- private_constant :EMPTY_HASH
9
-
10
7
  class << self
11
- def inherit_from(source, inherit_attributes: true)
12
- stacks = Bags.stack_sections.to_h do |section|
13
- [section, inherited_stack(source, section, inherit_section?(section, inherit_attributes))]
8
+ def inherit_from(source, inherit_attributes:)
9
+ stacks = Bags.stack_sections.each_with_object({}) do |section, inherited|
10
+ next unless inherit_section?(section, inherit_attributes)
11
+
12
+ inherited[section] = source.stack(section).branch
14
13
  end
15
14
  new(**stacks)
16
15
  end
@@ -20,16 +19,12 @@ module Julewire
20
19
  def inherit_section?(section, inherit_attributes)
21
20
  inherit_attributes || !%i[attributes neutral].include?(section)
22
21
  end
23
-
24
- def inherited_stack(source, section, inherit)
25
- inherit ? source.stack(section).fork : FieldStack.new
26
- end
27
22
  end
28
23
 
29
24
  def initialize(**sections)
30
25
  @stacks = Bags.stack_sections.to_h do |section|
31
- [section, field_stack(sections.fetch(section, EMPTY_HASH), section)]
32
- end.freeze
26
+ [section, field_stack(sections.fetch(section, nil), section)]
27
+ end
33
28
  end
34
29
 
35
30
  def stack(section)
@@ -48,8 +43,8 @@ module Julewire
48
43
  stack(section).delete(path)
49
44
  end
50
45
 
51
- def with(section, fields, owned: false, &)
52
- stack(section).with(fields, owned: owned, &)
46
+ def with(section, fields = nil, owned: false, **keyword_fields, &)
47
+ stack(section).with(fields, owned: owned, **keyword_fields, &)
53
48
  end
54
49
 
55
50
  def without(section, path, &)
@@ -19,7 +19,7 @@ module Julewire
19
19
  end
20
20
 
21
21
  def remove(key)
22
- Fields::Internal.delete_key!(@fields, key)
22
+ Internal.delete_key!(@fields, key)
23
23
  self
24
24
  end
25
25
 
@@ -28,7 +28,7 @@ module Julewire
28
28
  end
29
29
 
30
30
  def reset!
31
- @config = build_config
31
+ @config = nil
32
32
  end
33
33
 
34
34
  private
@@ -38,9 +38,10 @@ module Julewire
38
38
  end
39
39
 
40
40
  def validate_config!(configuration)
41
- return if configuration.is_a?(configuration_class)
41
+ klass = configuration_class
42
+ return if configuration.is_a?(klass)
42
43
 
43
- raise TypeError, "expected #{configuration_class.name}"
44
+ raise TypeError, "expected #{klass}"
44
45
  end
45
46
 
46
47
  def configuration_class
@@ -5,11 +5,11 @@ module Julewire
5
5
  module Integration
6
6
  # @api integration_spi
7
7
  class DestinationHealth
8
- def initialize(counter_keys:, failure_counter: :failures)
8
+ def initialize(counter_keys:, callback_failure_counter: nil, failure_counter: :failures)
9
9
  @failure_counter = failure_counter
10
10
  @state = Diagnostics::Health.new(
11
+ callback_failure_counter: callback_failure_counter,
11
12
  counter_keys: counter_keys,
12
- failure_counter: failure_counter,
13
13
  track_failures: failure_counter == :failures
14
14
  )
15
15
  end
@@ -19,23 +19,44 @@ module Julewire
19
19
  end
20
20
 
21
21
  def record_failure(error, counter: @failure_counter, **metadata)
22
- @state.record_failure(error, counter: counter, degrade: false, **metadata)
22
+ @state.record_failure(error, counter: counter, **metadata)
23
23
  end
24
24
 
25
25
  def record_loss(reason:, counter: reason, **metadata)
26
- @state.record_loss(reason: reason, counter: counter, degrade: false, **metadata)
26
+ @state.record_loss(reason: reason, counter: counter, **metadata)
27
27
  end
28
28
 
29
- def clear_degraded! = @state.clear_failures!
29
+ def record_callback_failure(callback_failure)
30
+ @state.record_callback_failure(callback_failure)
31
+ end
32
+
33
+ def degradation_marker = @state.degradation_marker
34
+
35
+ def clear_degradation_if_unchanged(marker) = @state.clear_degradation_if_unchanged(marker)
36
+
37
+ def recover_if_successful
38
+ marker = degradation_marker
39
+ result = yield
40
+ clear_degradation_if_unchanged(marker) unless result == false
41
+ result
42
+ end
43
+
44
+ def clear_failures! = @state.clear_failures!
30
45
 
31
- def degraded? = @state.degraded?(status_from: :failure_or_loss)
46
+ def degraded? = @state.degraded?
47
+
48
+ def last_callback_failure = @state.last_callback_failure
32
49
 
33
50
  def last_loss = @state.last_loss
34
51
 
35
52
  def last_failure = @state.last_failure
36
53
 
37
54
  def snapshot(status: nil, **fields)
38
- @state.snapshot(status: status, status_from: :failure_or_loss, include_loss: true, **fields)
55
+ snapshot = @state.snapshot(status: status, include_loss: true, **fields)
56
+ callback_failure = last_callback_failure
57
+ return snapshot unless callback_failure
58
+
59
+ snapshot.merge(last_callback_failure: callback_failure).freeze
39
60
  end
40
61
  end
41
62
  end