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,6 +4,9 @@ module Julewire
4
4
  module Core
5
5
  module Serialization
6
6
  class BacktraceLimiter
7
+ MAX_CAUSE_DEPTH = Core::NORMALIZATION_MAX_DEPTH
8
+ private_constant :MAX_CAUSE_DEPTH
9
+
7
10
  class << self
8
11
  def call(value, max_backtrace_lines:)
9
12
  new(max_backtrace_lines: max_backtrace_lines).call(value)
@@ -18,18 +21,18 @@ module Julewire
18
21
  end
19
22
 
20
23
  def call(value)
21
- @seen = {}.compare_by_identity
22
- limit_backtraces(value)
24
+ limit_backtraces(value, Set.new.compare_by_identity)
23
25
  value
24
- ensure
25
- @seen = nil
26
26
  end
27
27
 
28
28
  private
29
29
 
30
- def limit_backtraces(value)
31
- while value.is_a?(Hash) && !@seen.key?(value)
32
- @seen[value] = true
30
+ def limit_backtraces(value, seen)
31
+ MAX_CAUSE_DEPTH.times do
32
+ break unless value.is_a?(Hash)
33
+ break if seen.include?(value)
34
+
35
+ seen.add(value)
33
36
  limit_backtrace_field!(value)
34
37
  value = value[:cause]
35
38
  end
@@ -40,8 +43,9 @@ module Julewire
40
43
 
41
44
  if @max_backtrace_lines.zero?
42
45
  error.delete(:backtrace)
43
- elsif error[:backtrace].is_a?(Array)
44
- error[:backtrace] = error[:backtrace].first(@max_backtrace_lines)
46
+ else
47
+ backtrace = error.fetch(:backtrace)
48
+ error[:backtrace] = backtrace.first(@max_backtrace_lines) if backtrace.is_a?(Array)
45
49
  end
46
50
  end
47
51
  end
@@ -35,12 +35,11 @@ module Julewire
35
35
  @prepare_values = !@transform.nil?
36
36
  @track_paths = @prepare_values && !track_paths.equal?(false)
37
37
  end
38
+ private_class_method :new
38
39
 
39
40
  def call(value)
40
41
  @root = value
41
42
  walk(value)
42
- ensure
43
- @root = nil
44
43
  end
45
44
 
46
45
  private
@@ -49,6 +48,8 @@ module Julewire
49
48
  transformed = @transform.call(value, key: key, path: path, original: @root, depth: depth)
50
49
  transformed.equal?(CONTINUE) ? value : transformed
51
50
  end
51
+
52
+ def truncation_metadata(fields) = super(fields, key_style: :symbol)
52
53
  end
53
54
  end
54
55
  end
@@ -20,18 +20,17 @@ module Julewire
20
20
  max_array_items: DEFAULT_MAX_ARRAY_ITEMS,
21
21
  max_depth: DEFAULT_MAX_DEPTH,
22
22
  max_hash_keys: DEFAULT_MAX_HASH_KEYS,
23
- max_string_bytes: DEFAULT_MAX_STRING_BYTES
23
+ max_string_bytes: DEFAULT_MAX_STRING_BYTES,
24
+ key_style: :string
24
25
  )
25
- {
26
- "truncated" => true,
27
- "truncated_fields" => Array(fields).uniq,
28
- "limits" => {
29
- "max_array_items" => max_array_items,
30
- "max_depth" => max_depth,
31
- "max_hash_keys" => max_hash_keys,
32
- "max_string_bytes" => max_string_bytes
33
- }
34
- }
26
+ TruncationMetadata.build(
27
+ fields,
28
+ key_style: key_style,
29
+ max_array_items: max_array_items,
30
+ max_depth: max_depth,
31
+ max_hash_keys: max_hash_keys,
32
+ max_string_bytes: max_string_bytes
33
+ )
35
34
  end
36
35
  end
37
36
 
@@ -43,57 +42,52 @@ module Julewire
43
42
  @max_hash_keys = Validation.validate_integer_limit!(max_hash_keys, name: :max_hash_keys)
44
43
  @max_string_bytes = Validation.validate_integer_limit!(max_string_bytes, name: :max_string_bytes)
45
44
  @truncation_key = truncation_key
46
- @last_truncated = false
47
- @compact_empty = false
48
- @prepare_values = false
49
- @track_paths = false
50
45
  end
51
46
 
52
47
  private
53
48
 
54
49
  def walk(value)
55
- @last_truncated = false
56
50
  traverse(value) { |root, depth| walk_value(root, depth, nil, nil) }
57
51
  ensure
58
52
  @last_truncated = false
59
53
  end
60
54
 
61
55
  def walk_value(value, depth, key, path)
62
- @last_truncated = false
63
56
  value = prepare_value(value, depth, key, path) if @prepare_values
64
- return max_depth_value if depth >= @max_depth
57
+ return max_depth_value if depth == @max_depth
65
58
  return walk_container(value, depth, path) if value.is_a?(Array) || hash_like?(value)
66
59
 
67
- scalar_value(value, depth, key, path)
60
+ scalar_value(value, depth)
68
61
  rescue StandardError => e
69
- @last_truncated = false
70
62
  error_value(e)
71
63
  end
72
64
 
73
- def prepare_value(value, _depth, _key, _path) = value
74
-
75
65
  def hash_like?(value) = value.is_a?(Hash)
76
66
 
77
- def scalar_value(value, _depth, _key, _path)
78
- value.is_a?(String) ? string_value(value) : clear_truncated(value)
67
+ def scalar_value(value, _depth)
68
+ value.is_a?(String) ? string_value(value) : value
79
69
  end
80
70
 
81
71
  # Transform-stage errors must bubble so processors can fail closed.
82
- def error_value(error)
83
- raise error
72
+ def error_value(_error)
73
+ raise
84
74
  end
85
75
 
86
76
  def walk_container(value, depth, path)
87
- return circular_value if traversal_seen?(value)
77
+ seen = traversal_seen
78
+ return circular_value if seen.include?(value)
88
79
 
89
- with_marked_traversal_container(value) do
80
+ seen.add(value)
81
+ begin
90
82
  value.is_a?(Array) ? walk_array(value, depth, path) : walk_hash(value, depth, path)
83
+ ensure
84
+ seen.delete(value)
91
85
  end
92
86
  end
93
87
 
94
88
  def circular_value
95
89
  @last_truncated = true
96
- Core::CIRCULAR_REFERENCE
90
+ CIRCULAR_REFERENCE
97
91
  end
98
92
 
99
93
  def max_depth_value
@@ -101,7 +95,7 @@ module Julewire
101
95
  end
102
96
 
103
97
  def walk_hash(value, depth, path)
104
- return walk_compact_hash(value, depth, path) if @compact_empty
98
+ return walk_compact_hash(value, depth) if @compact_empty
105
99
 
106
100
  walk_full_hash(value, depth, path)
107
101
  end
@@ -110,121 +104,122 @@ module Julewire
110
104
  fields = nil
111
105
  result = {}
112
106
  track_paths = @track_paths
107
+ visited = 0
113
108
  value.each do |raw_key, item|
114
- if result.length >= @max_hash_keys
109
+ if visited == @max_hash_keys
115
110
  fields = append_truncation_field(fields, "hash_keys")
116
111
  break
117
112
  end
118
113
 
119
- child = walk_value(item, depth + 1, raw_key, track_paths ? path_for(path, raw_key) : nil)
120
- child_truncated = consume_truncated
114
+ visited += 1
115
+ child_path = path_for(path, raw_key) if track_paths
116
+ child = walk_value(item, depth + 1, raw_key, child_path)
121
117
  key = key_value(raw_key)
122
- key_truncated = consume_truncated
123
118
  result[key] = child
124
- fields = record_hash_truncation(fields, raw_key, key, key_truncated, child_truncated)
119
+ fields = record_hash_truncation(fields, key, consume_truncated)
125
120
  end
126
121
  finish_hash(result, fields)
127
122
  end
128
123
 
129
- def walk_compact_hash(value, depth, path)
124
+ def walk_compact_hash(value, depth)
130
125
  fields = nil
131
126
  result = {}
132
- track_paths = @track_paths
127
+ visited = 0
133
128
  value.each do |raw_key, item|
134
- next if raw_omitted_value?(item)
135
-
136
- child = walk_value(item, depth + 1, raw_key, track_paths ? path_for(path, raw_key) : nil)
137
- child_truncated = consume_truncated
138
- next if omitted_value?(child)
139
-
140
- if result.length >= @max_hash_keys
129
+ # The hash cap bounds visited input entries, not final output keys, so
130
+ # serialized-key collisions and compacted entries cannot hide work.
131
+ if visited == @max_hash_keys
141
132
  fields = append_truncation_field(fields, "hash_keys")
142
133
  break
143
134
  end
144
135
 
136
+ visited += 1
137
+ next if raw_omitted_value?(item)
138
+
139
+ child = walk_value(item, depth + 1, nil, nil)
140
+ next if omitted_value?(child)
141
+
145
142
  key = key_value(raw_key)
146
- key_truncated = consume_truncated
147
143
  result[key] = child
148
- fields = record_hash_truncation(fields, raw_key, key, key_truncated, child_truncated)
144
+ fields = record_hash_truncation(fields, key, consume_truncated)
149
145
  end
150
146
  finish_hash(result, fields)
151
147
  end
152
148
 
153
149
  def walk_array(value, depth, path)
154
- return walk_compact_array(value, depth, path) if @compact_empty
150
+ return walk_compact_array(value, depth) if @compact_empty
155
151
 
156
152
  walk_full_array(value, depth, path)
157
153
  end
158
154
 
159
155
  def walk_full_array(value, depth, path)
160
- fields = nil
156
+ truncated = false
161
157
  result = []
158
+ visited = 0
162
159
  value.each do |item|
163
- if result.length >= @max_array_items
164
- fields = append_truncation_field(fields, "array_items")
160
+ if visited == @max_array_items
161
+ truncated = true
165
162
  break
166
163
  end
167
164
 
165
+ visited += 1
168
166
  child = walk_value(item, depth + 1, nil, path)
169
167
  child_truncated = consume_truncated
170
168
  result << child
171
- fields = append_truncation_field(fields, "array_items") if child_truncated
169
+ truncated = true if child_truncated
172
170
  end
173
- finish_array(result, fields)
171
+ finish_array(result, truncated && "array_items")
174
172
  end
175
173
 
176
- def walk_compact_array(value, depth, path)
177
- fields = nil
174
+ def walk_compact_array(value, depth)
175
+ truncated = false
178
176
  result = []
177
+ visited = 0
179
178
  value.each do |item|
179
+ if visited == @max_array_items
180
+ truncated = true
181
+ break
182
+ end
183
+
184
+ visited += 1
180
185
  next if raw_omitted_value?(item)
181
186
 
182
- child = walk_value(item, depth + 1, nil, path)
187
+ child = walk_value(item, depth + 1, nil, nil)
183
188
  child_truncated = consume_truncated
184
189
  next if omitted_value?(child)
185
190
 
186
- if result.length >= @max_array_items
187
- fields = append_truncation_field(fields, "array_items")
188
- break
189
- end
190
-
191
191
  result << child
192
- fields = append_truncation_field(fields, "array_items") if child_truncated
192
+ truncated = true if child_truncated
193
193
  end
194
- finish_array(result, fields)
194
+ finish_array(result, truncated && "array_items")
195
195
  end
196
196
 
197
- def omitted_value?(_value) = false
198
-
199
- def raw_omitted_value?(_value) = false
200
-
201
- def key_value(key)
202
- clear_truncated(key.is_a?(String) ? copy_string(key) : key)
203
- end
197
+ def key_value(key) = key
204
198
 
205
- def record_hash_truncation(fields, raw_key, _key, _key_truncated, child_truncated)
206
- return fields unless child_truncated
199
+ def record_hash_truncation(fields, key, truncated)
200
+ return fields unless truncated
207
201
 
208
- append_truncation_field(fields, raw_key.to_s)
202
+ append_truncation_field(fields, key.to_s)
209
203
  end
210
204
 
211
205
  def finish_hash(result, fields)
212
- return clear_truncated(result) unless fields
206
+ return result unless fields
213
207
 
214
208
  result[@truncation_key] = truncation_metadata(fields) if @truncation_key
215
209
  mark_truncated(result)
216
210
  end
217
211
 
218
212
  def finish_array(result, fields)
219
- return clear_truncated(result) unless fields
213
+ return result unless fields
220
214
 
221
215
  result << { @truncation_key => truncation_metadata(fields) } if @truncation_key
222
216
  mark_truncated(result)
223
217
  end
224
218
 
225
- def truncation_metadata(fields)
219
+ def truncation_metadata(fields, key_style: :string)
226
220
  self.class.truncation_metadata(
227
221
  fields,
222
+ key_style: key_style,
228
223
  max_array_items: @max_array_items,
229
224
  max_depth: @max_depth,
230
225
  max_hash_keys: @max_hash_keys,
@@ -233,7 +228,7 @@ module Julewire
233
228
  end
234
229
 
235
230
  def string_value(value)
236
- return clear_truncated(copy_string(value)) if value.bytesize <= @max_string_bytes
231
+ return copy_string(value) if value.bytesize <= @max_string_bytes
237
232
 
238
233
  mark_truncated("#{value.byteslice(0, @max_string_bytes).scrub("?")}#{TRUNCATED_SUFFIX}")
239
234
  end
@@ -243,11 +238,6 @@ module Julewire
243
238
  value
244
239
  end
245
240
 
246
- def clear_truncated(value)
247
- @last_truncated = false
248
- value
249
- end
250
-
251
241
  def consume_truncated
252
242
  truncated = @last_truncated
253
243
  @last_truncated = false
@@ -259,8 +249,7 @@ module Julewire
259
249
  end
260
250
 
261
251
  def append_truncation_field(fields, field)
262
- (fields ||= []) << field
263
- fields
252
+ TruncationMetadata.append_field(fields, field)
264
253
  end
265
254
 
266
255
  def path_for(parent_path, key)
@@ -27,23 +27,21 @@ module Julewire
27
27
  private
28
28
 
29
29
  def compact_value!(value)
30
- return compact_hash!(value) if value.is_a?(Hash)
31
- return compact_array!(value) if value.is_a?(Array)
32
-
33
- value
30
+ if value.is_a?(Hash)
31
+ compact_hash!(value)
32
+ elsif value.is_a?(Array)
33
+ compact_array!(value)
34
+ else
35
+ value
36
+ end
34
37
  end
35
38
 
36
39
  def compact_hash!(value)
37
40
  with_traversal_container(value, value) do
38
41
  value.each do |key, item|
39
42
  compacted = compact_value!(item)
40
- if self.class.omitted?(compacted)
41
- value.delete(key)
42
- elsif !compacted.equal?(item)
43
- value[key] = compacted
44
- end
43
+ value.delete(key) if self.class.omitted?(compacted)
45
44
  end
46
- value
47
45
  end
48
46
  end
49
47
 
@@ -57,7 +55,7 @@ module Julewire
57
55
  value[index] = compacted
58
56
  index += 1
59
57
  end
60
- value.slice!(index, value.length - index) if index < value.length
58
+ value.slice!(index, value.length)
61
59
  value
62
60
  end
63
61
  end
@@ -7,9 +7,46 @@ module Julewire
7
7
  include ValueTraversal
8
8
 
9
9
  class << self
10
- def call(value, max_depth: Core::NORMALIZATION_MAX_DEPTH, trust_frozen: false)
10
+ def call(value, max_depth: NORMALIZATION_MAX_DEPTH, trust_frozen: false, validate_symbol_keys: false)
11
+ validate_symbol_keys(value) if validate_symbol_keys
11
12
  new(max_depth, trust_frozen: trust_frozen).call(value)
12
13
  end
14
+
15
+ def validate_symbol_keys(value)
16
+ # WeakMap is identity-keyed and accepts new keys while enumerating,
17
+ # so reachable containers form one finite work inventory without a
18
+ # separate cycle-skip branch.
19
+ containers = ObjectSpace::WeakMap.new
20
+ containers[value] = nil if container?(value)
21
+ containers.each_key do |current|
22
+ if current.is_a?(Hash)
23
+ current.each do |key, item|
24
+ validate_symbol_key!(key)
25
+ containers[item] = nil if container?(item)
26
+ end
27
+ else
28
+ current.each { containers[it] = nil if container?(it) }
29
+ end
30
+ end
31
+ value
32
+ end
33
+
34
+ def validate_symbol_hash(value)
35
+ raise TypeError, "owned data must be a Hash" unless value.is_a?(Hash)
36
+
37
+ validate_symbol_keys(value)
38
+ end
39
+
40
+ private
41
+
42
+ def container?(value)
43
+ value.is_a?(Hash) || value.is_a?(Array)
44
+ end
45
+
46
+ def validate_symbol_key!(key)
47
+ raise TypeError, Fields::Internal::RECORD_STRING_KEY_ERROR if key.is_a?(String)
48
+ raise TypeError, Fields::Internal::RECORD_SYMBOL_KEY_ERROR unless key.instance_of?(Symbol)
49
+ end
13
50
  end
14
51
 
15
52
  def initialize(max_depth, trust_frozen:)
@@ -24,7 +61,7 @@ module Julewire
24
61
  private
25
62
 
26
63
  def freeze_value(value, depth)
27
- return value.freeze if value.is_a?(String)
64
+ value.freeze if value.is_a?(String)
28
65
  return value if @trust_frozen && value.frozen?
29
66
  return freeze_container(value, depth) if value.is_a?(Hash) || value.is_a?(Array)
30
67
 
@@ -32,7 +69,7 @@ module Julewire
32
69
  end
33
70
 
34
71
  def freeze_container(value, depth)
35
- return Serializer::MAX_DEPTH_VALUE.freeze if depth_limited?(depth)
72
+ return Serializer::MAX_DEPTH_VALUE if depth_limited?(depth)
36
73
 
37
74
  with_traversal_container(value, value) do
38
75
  value.is_a?(Hash) ? freeze_hash(value, depth) : freeze_array(value, depth)
@@ -40,11 +77,13 @@ module Julewire
40
77
  end
41
78
 
42
79
  def depth_limited?(depth)
43
- @max_depth && depth >= @max_depth
80
+ depth == @max_depth
44
81
  end
45
82
 
46
83
  def freeze_hash(value, depth)
47
- value.each { |key, item| freeze_child(value, key, item, depth) }
84
+ value.each do |key, item|
85
+ freeze_child(value, key, item, depth)
86
+ end
48
87
  value.freeze
49
88
  end
50
89
 
@@ -55,7 +94,7 @@ module Julewire
55
94
 
56
95
  def freeze_child(value, key, item, depth)
57
96
  frozen = freeze_value(item, depth + 1)
58
- value[key] = frozen unless value.frozen? || frozen.equal?(item)
97
+ value[key] = frozen unless value.frozen?
59
98
  end
60
99
  end
61
100
  end
@@ -9,7 +9,6 @@ module Julewire
9
9
  raise TypeError, "value must be a String" unless value.is_a?(String)
10
10
 
11
11
  return value if valid_utf8?(value) || valid_ascii_only?(value)
12
- return value.scrub("?") if utf8?(value)
13
12
 
14
13
  encode_utf8(value)
15
14
  rescue EncodingError
@@ -23,7 +22,7 @@ module Julewire
23
22
  end
24
23
 
25
24
  def valid_ascii_only?(value)
26
- value.ascii_only? && value.valid_encoding?
25
+ value.ascii_only?
27
26
  end
28
27
 
29
28
  def utf8?(value)
@@ -31,7 +30,7 @@ module Julewire
31
30
  end
32
31
 
33
32
  def encode_utf8(value)
34
- value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?")
33
+ value.encode(Encoding::UTF_8, invalid: :replace, replace: "?")
35
34
  end
36
35
  end
37
36
  end
@@ -9,7 +9,7 @@ module Julewire
9
9
  DEFAULT_MAX_CAUSE_DEPTH = 5
10
10
 
11
11
  class << self
12
- def call(error, max_backtrace_lines: Core::MAX_BACKTRACE_LINES, max_cause_depth: DEFAULT_MAX_CAUSE_DEPTH)
12
+ def call(error, max_backtrace_lines: MAX_BACKTRACE_LINES, max_cause_depth: DEFAULT_MAX_CAUSE_DEPTH)
13
13
  new(
14
14
  max_backtrace_lines: max_backtrace_lines,
15
15
  max_cause_depth: max_cause_depth
@@ -32,7 +32,7 @@ module Julewire
32
32
  def shape_exception(error, depth)
33
33
  return error unless error.is_a?(Exception)
34
34
 
35
- with_traversal_container(error, Core::CIRCULAR_REFERENCE) do
35
+ with_traversal_container(error, CIRCULAR_REFERENCE) do
36
36
  exception_hash(error, depth)
37
37
  end
38
38
  end
@@ -50,7 +50,7 @@ module Julewire
50
50
  cause = exception_cause(error)
51
51
  next unless cause
52
52
 
53
- if depth >= @max_cause_depth
53
+ if depth == @max_cause_depth
54
54
  result[:cause_truncated] = true
55
55
  else
56
56
  result[:cause] = shape_exception(cause, depth + 1)
@@ -59,20 +59,21 @@ module Julewire
59
59
  end
60
60
 
61
61
  def class_name(error)
62
- error.class.name || error.class.to_s
62
+ error.class.to_s
63
63
  rescue StandardError
64
64
  "Exception"
65
65
  end
66
66
 
67
67
  def error_message(error)
68
68
  message = error.message
69
- message.is_a?(String) ? message.dup : message.to_s
69
+ message.instance_of?(String) ? message.dup : message.to_s
70
70
  rescue StandardError
71
71
  "[Unavailable]"
72
72
  end
73
73
 
74
74
  def backtrace(error)
75
- @backtrace_limiter.call(backtrace: Core::Fields::FieldSet.deep_dup(error.backtrace))[:backtrace]
75
+ limited = @backtrace_limiter.call(backtrace: Fields::FieldSet.deep_dup(error.backtrace))
76
+ limited.fetch(:backtrace)
76
77
  rescue StandardError
77
78
  nil
78
79
  end
@@ -13,7 +13,7 @@ module Julewire
13
13
  max_array_items: Serializer::DEFAULT_MAX_ARRAY_ITEMS,
14
14
  max_hash_keys: Serializer::DEFAULT_MAX_HASH_KEYS,
15
15
  compact_empty: true,
16
- max_backtrace_lines: Core::MAX_BACKTRACE_LINES,
16
+ max_backtrace_lines: MAX_BACKTRACE_LINES,
17
17
  append_newline: true
18
18
  )
19
19
  @max_depth = max_depth
@@ -23,6 +23,7 @@ module Julewire
23
23
  @compact_empty = compact_empty
24
24
  @max_backtrace_lines = max_backtrace_lines
25
25
  @line_suffix = append_newline ? "\n" : ""
26
+ # `append_newline` is suffix-only; it does not change serializer shape.
26
27
  @serializer_key = [
27
28
  @max_depth,
28
29
  @max_string_bytes,
@@ -30,11 +31,11 @@ module Julewire
30
31
  @max_hash_keys,
31
32
  @compact_empty,
32
33
  @max_backtrace_lines
33
- ].freeze
34
+ ]
34
35
  end
35
36
 
36
37
  def call(payload)
37
- JSON.generate(serialized_payload(payload), allow_nan: false).tap do |json|
38
+ JSON.generate(serialized_payload(payload)).tap do |json|
38
39
  json << @line_suffix
39
40
  end
40
41
  end
@@ -42,14 +43,11 @@ module Julewire
42
43
  private
43
44
 
44
45
  def serialized_payload(payload)
45
- serializer = cached_serializer
46
- return build_serializer.serialize(payload) if serializer.in_use?
47
-
48
- serializer.serialize(payload)
49
- end
50
-
51
- def cached_serializer
52
- SerializerPool.serializer(:julewire_core_json_encoder_serializers, @serializer_key) { build_serializer }
46
+ SerializerPool.serialize(
47
+ :julewire_core_json_encoder_serializers,
48
+ @serializer_key,
49
+ payload
50
+ ) { build_serializer }
53
51
  end
54
52
 
55
53
  def build_serializer
@@ -59,8 +57,7 @@ module Julewire
59
57
  max_array_items: @max_array_items,
60
58
  max_hash_keys: @max_hash_keys,
61
59
  compact_empty: @compact_empty,
62
- max_backtrace_lines: @max_backtrace_lines,
63
- copy_strings: false
60
+ max_backtrace_lines: @max_backtrace_lines
64
61
  )
65
62
  end
66
63
  end