julewire-core 1.0.0 → 1.0.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/docs/advanced-configuration.md +2 -1
- data/docs/context-and-propagation.md +6 -4
- data/docs/contracts.md +2 -1
- data/docs/extensions-and-api.md +9 -6
- data/docs/internals.md +3 -3
- data/docs/outputs-and-lifecycle.md +5 -2
- data/docs/records-and-data-policy.md +2 -2
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
- data/lib/julewire/core/context_store.rb +11 -6
- data/lib/julewire/core/destinations/destination.rb +3 -3
- data/lib/julewire/core/destinations/synchronized_output.rb +51 -15
- data/lib/julewire/core/diagnostics/doctor.rb +11 -11
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +3 -1
- data/lib/julewire/core/execution/handle.rb +2 -0
- data/lib/julewire/core/execution/lineage.rb +10 -6
- data/lib/julewire/core/execution/scope.rb +4 -4
- data/lib/julewire/core/execution/scope_fields.rb +2 -2
- data/lib/julewire/core/facade_methods.rb +2 -2
- data/lib/julewire/core/fields/field_set.rb +32 -6
- data/lib/julewire/core/fields/field_stack.rb +40 -14
- data/lib/julewire/core/fields/internal.rb +41 -13
- data/lib/julewire/core/fields/stack_set.rb +2 -2
- data/lib/julewire/core/integration/destination_health.rb +13 -2
- data/lib/julewire/core/propagation/carrier.rb +63 -9
- data/lib/julewire/core/propagation.rb +3 -2
- data/lib/julewire/core/records/draft.rb +12 -6
- data/lib/julewire/core/records/record.rb +43 -13
- data/lib/julewire/core/runtime.rb +30 -11
- data/lib/julewire/core/serialization/bounded_transform.rb +11 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +33 -27
- data/lib/julewire/core/serialization/serializer.rb +1 -1
- data/lib/julewire/core/serialization/truncation_metadata.rb +142 -0
- data/lib/julewire/core/serialization/value_copy.rb +292 -51
- data/lib/julewire/core/testing/contracts/integration.rb +1 -1
- data/lib/julewire/core/version.rb +1 -1
- metadata +2 -1
|
@@ -13,17 +13,20 @@ 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
|
+
def initialize(parent, fields, delete_paths: nil, clear_parent_deletes: true, owned: false)
|
|
17
17
|
@parent = parent
|
|
18
18
|
@fields = fields
|
|
19
19
|
@delete_paths = delete_paths
|
|
20
20
|
@clear_parent_deletes = clear_parent_deletes
|
|
21
|
+
@owned = owned
|
|
21
22
|
@active_delete_paths_computed = false
|
|
22
23
|
@active_delete_paths = nil
|
|
23
24
|
@snapshot = nil
|
|
24
25
|
@value_cache = nil
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
def owned? = @owned
|
|
29
|
+
|
|
27
30
|
def snapshot
|
|
28
31
|
@snapshot ||= build_snapshot
|
|
29
32
|
end
|
|
@@ -35,7 +38,7 @@ module Julewire
|
|
|
35
38
|
FieldSet.value_for(snapshot, key, default: MISSING)
|
|
36
39
|
else
|
|
37
40
|
field_value = FieldSet.value_for(@fields, key, default: MISSING)
|
|
38
|
-
field_value.equal?(MISSING) ? parent_value_for(key) :
|
|
41
|
+
field_value.equal?(MISSING) ? parent_value_for(key) : frozen_field_value(field_value)
|
|
39
42
|
end
|
|
40
43
|
(@value_cache ||= {})[key] = value
|
|
41
44
|
end
|
|
@@ -56,6 +59,14 @@ module Julewire
|
|
|
56
59
|
@clear_parent_deletes ? @delete_paths : active_delete_paths
|
|
57
60
|
end
|
|
58
61
|
|
|
62
|
+
def merge_into(snapshot)
|
|
63
|
+
if @owned
|
|
64
|
+
Fields::Internal.merge_owned!(snapshot, FieldSet.deep_symbolize_owned_keys(@fields))
|
|
65
|
+
else
|
|
66
|
+
FieldSet.merge!(snapshot, @fields)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
private
|
|
60
71
|
|
|
61
72
|
def build_snapshot
|
|
@@ -64,30 +75,35 @@ module Julewire
|
|
|
64
75
|
|
|
65
76
|
snapshot = source_snapshot_base
|
|
66
77
|
source_chain.reverse_each do |source|
|
|
67
|
-
|
|
78
|
+
source.merge_into(snapshot)
|
|
68
79
|
paths = source.delete_paths_for_snapshot
|
|
69
80
|
Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
|
|
70
81
|
end
|
|
71
|
-
Fields::Internal.
|
|
82
|
+
Fields::Internal.frozen_owned_copy(snapshot)
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
def build_direct_snapshot
|
|
75
|
-
snapshot =
|
|
86
|
+
snapshot = merge_into({})
|
|
76
87
|
paths = delete_paths_for_snapshot
|
|
77
88
|
Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
|
|
78
|
-
Fields::Internal.
|
|
89
|
+
Fields::Internal.frozen_owned_copy(snapshot)
|
|
79
90
|
end
|
|
80
91
|
|
|
81
92
|
def build_parent_snapshot
|
|
82
|
-
snapshot = FieldSet.
|
|
93
|
+
snapshot = FieldSet.deep_dup_owned(@parent.snapshot)
|
|
94
|
+
merge_into(snapshot)
|
|
83
95
|
paths = delete_paths_for_snapshot
|
|
84
96
|
Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
|
|
85
|
-
Fields::Internal.
|
|
97
|
+
Fields::Internal.frozen_owned_copy(snapshot)
|
|
86
98
|
end
|
|
87
99
|
|
|
88
100
|
def source_snapshot_base
|
|
89
101
|
source = source_chain_base
|
|
90
|
-
source ? FieldSet.
|
|
102
|
+
source ? FieldSet.deep_dup_owned(source.snapshot) : {}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def frozen_field_value(value)
|
|
106
|
+
@owned ? Fields::Internal.frozen_owned_copy(value) : Fields::Internal.frozen_copy(value)
|
|
91
107
|
end
|
|
92
108
|
|
|
93
109
|
def source_chain
|
|
@@ -181,7 +197,7 @@ module Julewire
|
|
|
181
197
|
return if fields.empty?
|
|
182
198
|
|
|
183
199
|
fields = normalize_owned_keys(fields) if owned
|
|
184
|
-
@source = Layer.new(@source, fields, clear_parent_deletes: true)
|
|
200
|
+
@source = Layer.new(@source, fields, clear_parent_deletes: true, owned: owned)
|
|
185
201
|
invalidate_snapshot!
|
|
186
202
|
end
|
|
187
203
|
|
|
@@ -199,7 +215,7 @@ module Julewire
|
|
|
199
215
|
return yield if fields.empty?
|
|
200
216
|
|
|
201
217
|
fields = normalize_owned_keys(fields) if owned
|
|
202
|
-
with_layer(fields, &)
|
|
218
|
+
with_layer(fields, owned: owned, &)
|
|
203
219
|
end
|
|
204
220
|
|
|
205
221
|
def without(path, &)
|
|
@@ -226,9 +242,15 @@ module Julewire
|
|
|
226
242
|
FieldSet.coerce(fields, keyword_fields)
|
|
227
243
|
end
|
|
228
244
|
|
|
229
|
-
def with_layer(fields, delete_paths: nil)
|
|
245
|
+
def with_layer(fields, delete_paths: nil, owned: false)
|
|
230
246
|
previous_source = @source
|
|
231
|
-
@source = Layer.new(
|
|
247
|
+
@source = Layer.new(
|
|
248
|
+
previous_source,
|
|
249
|
+
fields,
|
|
250
|
+
delete_paths: delete_paths,
|
|
251
|
+
clear_parent_deletes: false,
|
|
252
|
+
owned: owned
|
|
253
|
+
)
|
|
232
254
|
invalidate_snapshot!
|
|
233
255
|
begin
|
|
234
256
|
yield
|
|
@@ -250,13 +272,17 @@ module Julewire
|
|
|
250
272
|
unless @source.parent
|
|
251
273
|
# Single-layer hits avoid Layer's delete-path/cache bookkeeping.
|
|
252
274
|
field_value = FieldSet.value_for(@source.fields, key, default: MISSING)
|
|
253
|
-
return
|
|
275
|
+
return frozen_source_value(field_value, @source.owned?) unless field_value.equal?(MISSING)
|
|
254
276
|
end
|
|
255
277
|
|
|
256
278
|
value = @source.value_for(key)
|
|
257
279
|
value.equal?(MISSING) ? MISSING : value
|
|
258
280
|
end
|
|
259
281
|
|
|
282
|
+
def frozen_source_value(value, owned)
|
|
283
|
+
owned ? Fields::Internal.frozen_owned_copy(value) : Fields::Internal.frozen_copy(value)
|
|
284
|
+
end
|
|
285
|
+
|
|
260
286
|
def invalidate_snapshot!
|
|
261
287
|
@version += 1
|
|
262
288
|
@snapshot = nil
|
|
@@ -18,12 +18,29 @@ module Julewire
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def frozen_copy(value)
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
frozen_copy_with(value, preserve_truncation_metadata: false)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def frozen_owned_copy(value)
|
|
25
|
+
frozen_copy_with(value, preserve_truncation_metadata: true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def frozen_deep_symbolize_keys(value)
|
|
29
|
+
frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata: false)
|
|
30
|
+
end
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
def frozen_deep_symbolize_owned_keys(value)
|
|
33
|
+
frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata: true)
|
|
25
34
|
end
|
|
26
35
|
|
|
36
|
+
def delete_path!(target, path) = Deletion.delete_path!(target, path)
|
|
37
|
+
|
|
38
|
+
def apply_delete_paths!(target, paths) = Deletion.apply_delete_paths!(target, paths)
|
|
39
|
+
|
|
40
|
+
def clear_delete_paths!(paths, fields) = Deletion.clear_delete_paths!(paths, fields)
|
|
41
|
+
|
|
42
|
+
def normalize_path(path) = Deletion.normalize_path(path)
|
|
43
|
+
|
|
27
44
|
def deep_merge(left, right)
|
|
28
45
|
deep_merge!(FieldSet.deep_symbolize_keys(left), right)
|
|
29
46
|
end
|
|
@@ -52,22 +69,33 @@ module Julewire
|
|
|
52
69
|
merge_values!(target, fields) { |value, _existing| value }
|
|
53
70
|
end
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def frozen_copy_with(value, preserve_truncation_metadata:)
|
|
56
75
|
return EMPTY_HASH if value.is_a?(Hash) && value.empty?
|
|
57
76
|
return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
|
|
58
77
|
|
|
59
|
-
Serialization::ValueCopy.call(
|
|
78
|
+
Serialization::ValueCopy.call(
|
|
79
|
+
value,
|
|
80
|
+
freeze_values: true,
|
|
81
|
+
preserve_truncation_metadata: preserve_truncation_metadata
|
|
82
|
+
)
|
|
60
83
|
end
|
|
61
84
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def clear_delete_paths!(paths, fields) = Deletion.clear_delete_paths!(paths, fields)
|
|
67
|
-
|
|
68
|
-
def normalize_path(path) = Deletion.normalize_path(path)
|
|
85
|
+
def frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata:)
|
|
86
|
+
return EMPTY_HASH if value.is_a?(Hash) && value.empty?
|
|
87
|
+
return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
Serialization::ValueCopy.call(
|
|
90
|
+
value,
|
|
91
|
+
freeze_values: true,
|
|
92
|
+
max_array_items: Serialization::Serializer::DEFAULT_MAX_ARRAY_ITEMS,
|
|
93
|
+
max_hash_keys: Serialization::Serializer::DEFAULT_MAX_HASH_KEYS,
|
|
94
|
+
max_string_bytes: Serialization::Serializer::DEFAULT_MAX_STRING_BYTES,
|
|
95
|
+
preserve_truncation_metadata: preserve_truncation_metadata,
|
|
96
|
+
symbolize_keys: true
|
|
97
|
+
)
|
|
98
|
+
end
|
|
71
99
|
|
|
72
100
|
def merge_values!(target, fields)
|
|
73
101
|
return target unless fields.is_a?(Hash)
|
|
@@ -48,8 +48,8 @@ module Julewire
|
|
|
48
48
|
stack(section).delete(path)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
def with(section, fields, owned: false, &)
|
|
52
|
-
stack(section).with(fields, owned: owned, &)
|
|
51
|
+
def with(section, fields = nil, owned: false, **keyword_fields, &)
|
|
52
|
+
stack(section).with(fields, owned: owned, **keyword_fields, &)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def without(section, path, &)
|
|
@@ -5,9 +5,10 @@ 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
13
|
failure_counter: failure_counter,
|
|
13
14
|
track_failures: failure_counter == :failures
|
|
@@ -26,16 +27,26 @@ module Julewire
|
|
|
26
27
|
@state.record_loss(reason: reason, counter: counter, degrade: false, **metadata)
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
def record_callback_failure(callback_failure)
|
|
31
|
+
@state.record_callback_failure(callback_failure)
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def clear_degraded! = @state.clear_failures!
|
|
30
35
|
|
|
31
36
|
def degraded? = @state.degraded?(status_from: :failure_or_loss)
|
|
32
37
|
|
|
38
|
+
def last_callback_failure = @state.last_callback_failure
|
|
39
|
+
|
|
33
40
|
def last_loss = @state.last_loss
|
|
34
41
|
|
|
35
42
|
def last_failure = @state.last_failure
|
|
36
43
|
|
|
37
44
|
def snapshot(status: nil, **fields)
|
|
38
|
-
@state.snapshot(status: status, status_from: :failure_or_loss, include_loss: true, **fields)
|
|
45
|
+
snapshot = @state.snapshot(status: status, status_from: :failure_or_loss, include_loss: true, **fields)
|
|
46
|
+
callback_failure = @state.last_callback_failure
|
|
47
|
+
return snapshot unless callback_failure
|
|
48
|
+
|
|
49
|
+
snapshot.merge(last_callback_failure: callback_failure).freeze
|
|
39
50
|
end
|
|
40
51
|
end
|
|
41
52
|
end
|
|
@@ -8,8 +8,33 @@ module Julewire
|
|
|
8
8
|
# @api public
|
|
9
9
|
module Carrier
|
|
10
10
|
DEFAULT_KEY = "julewire"
|
|
11
|
+
DEFAULT_MAX_BYTES = 65_536
|
|
11
12
|
DEFAULT_ENVELOPE = Core.sentinel(:default_envelope)
|
|
13
|
+
class Extracted
|
|
14
|
+
attr_reader :envelope, :status, :reason, :error
|
|
15
|
+
|
|
16
|
+
def initialize(envelope:, status:, reason: nil, error: nil)
|
|
17
|
+
@envelope = envelope
|
|
18
|
+
@status = status
|
|
19
|
+
@reason = reason
|
|
20
|
+
@error = error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def failure? = !error.nil?
|
|
24
|
+
end
|
|
12
25
|
private_constant :DEFAULT_ENVELOPE
|
|
26
|
+
private_constant :Extracted
|
|
27
|
+
|
|
28
|
+
# @api integration_spi
|
|
29
|
+
class ExtractionError < StandardError
|
|
30
|
+
attr_reader :status, :reason
|
|
31
|
+
|
|
32
|
+
def initialize(status, reason)
|
|
33
|
+
@status = status
|
|
34
|
+
@reason = reason
|
|
35
|
+
super(reason)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
13
38
|
|
|
14
39
|
class << self
|
|
15
40
|
def encode(envelope: DEFAULT_ENVELOPE, max_bytes: nil)
|
|
@@ -31,18 +56,20 @@ module Julewire
|
|
|
31
56
|
carrier
|
|
32
57
|
end
|
|
33
58
|
|
|
34
|
-
def extract(carrier, key: DEFAULT_KEY)
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
def extract(carrier, key: DEFAULT_KEY, max_bytes: DEFAULT_MAX_BYTES)
|
|
60
|
+
extract_result(carrier, key: key, max_bytes: max_bytes).envelope
|
|
61
|
+
end
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
# @api integration_spi
|
|
64
|
+
def extract_result(carrier, key: DEFAULT_KEY, max_bytes: DEFAULT_MAX_BYTES)
|
|
65
|
+
Validation.validate_byte_limit!(max_bytes, name: :max_bytes)
|
|
66
|
+
|
|
67
|
+
extract_payload(carrier, key: key, max_bytes: max_bytes)
|
|
42
68
|
end
|
|
43
69
|
|
|
44
|
-
def restore(carrier, key: DEFAULT_KEY, link_executions: false, &)
|
|
45
|
-
|
|
70
|
+
def restore(carrier, key: DEFAULT_KEY, link_executions: false, max_bytes: DEFAULT_MAX_BYTES, &)
|
|
71
|
+
envelope = extract_result(carrier, key: key, max_bytes: max_bytes).envelope
|
|
72
|
+
Propagation.restore(envelope, link_executions: link_executions, owned: true, &)
|
|
46
73
|
end
|
|
47
74
|
|
|
48
75
|
def serialized_envelope(envelope)
|
|
@@ -53,6 +80,33 @@ module Julewire
|
|
|
53
80
|
|
|
54
81
|
private
|
|
55
82
|
|
|
83
|
+
def extract_payload(carrier, key:, max_bytes:)
|
|
84
|
+
value = carrier_value(carrier, key)
|
|
85
|
+
return extracted({}, :missing) unless value
|
|
86
|
+
|
|
87
|
+
string = value.to_s
|
|
88
|
+
if max_bytes && string.bytesize > max_bytes
|
|
89
|
+
return extracted_failure(:oversized, "carrier payload exceeds max_bytes")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
parsed = JSON.parse(string)
|
|
93
|
+
return extracted_failure(:non_hash, "carrier payload must be a JSON object") unless parsed.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
extracted(Fields::FieldSet.deep_symbolize_owned_keys(parsed), :ok)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
extracted_failure(:malformed, "carrier payload is not valid JSON", e)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extracted(envelope, status)
|
|
101
|
+
Extracted.new(envelope: envelope, status: status)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def extracted_failure(status, reason, cause = nil)
|
|
105
|
+
error = ExtractionError.new(status, reason)
|
|
106
|
+
error.set_backtrace(cause.backtrace) if cause&.backtrace
|
|
107
|
+
Extracted.new(envelope: {}, status: status, reason: reason, error: error)
|
|
108
|
+
end
|
|
109
|
+
|
|
56
110
|
def carrier_value(carrier, key)
|
|
57
111
|
return unless carrier.respond_to?(:[])
|
|
58
112
|
|
|
@@ -13,10 +13,10 @@ module Julewire
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def capture_local
|
|
16
|
-
capture_with { Fields::FieldSet.
|
|
16
|
+
capture_with { Fields::FieldSet.deep_dup_owned(it) }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def restore(envelope, link_executions: false, &)
|
|
19
|
+
def restore(envelope, link_executions: false, owned: false, &)
|
|
20
20
|
raise ArgumentError, "block required" unless block_given?
|
|
21
21
|
|
|
22
22
|
sections = FIELD_SECTIONS.to_h { |section| [section, hash_value(envelope, section)] }
|
|
@@ -25,6 +25,7 @@ module Julewire
|
|
|
25
25
|
**sections,
|
|
26
26
|
execution: execution,
|
|
27
27
|
link_executions: link_executions,
|
|
28
|
+
owned: owned,
|
|
28
29
|
&
|
|
29
30
|
)
|
|
30
31
|
end
|
|
@@ -90,10 +90,11 @@ module Julewire
|
|
|
90
90
|
public
|
|
91
91
|
|
|
92
92
|
def from_normalized_hash(data, lineage: nil, freeze_sections: true)
|
|
93
|
-
|
|
93
|
+
Record.validate_normalized_hash!(data)
|
|
94
|
+
normalized = data.dup
|
|
94
95
|
lineage ||= Execution::Lineage.from_execution_hash(normalized[:execution])
|
|
95
|
-
normalized[:execution] = Execution::Lineage.
|
|
96
|
-
normalized = Fields::Internal.
|
|
96
|
+
normalized[:execution] = Execution::Lineage.clean_normalized_lazy_relationship_hash(normalized[:execution])
|
|
97
|
+
normalized = Fields::Internal.frozen_owned_copy(normalized) if freeze_sections
|
|
97
98
|
new(
|
|
98
99
|
normalized,
|
|
99
100
|
lineage: lineage,
|
|
@@ -182,7 +183,7 @@ module Julewire
|
|
|
182
183
|
|
|
183
184
|
def each_key(&) = @data.each_key(&)
|
|
184
185
|
|
|
185
|
-
def to_h = Fields::FieldSet.
|
|
186
|
+
def to_h = Fields::FieldSet.deep_dup_owned(@data)
|
|
186
187
|
|
|
187
188
|
def transform_field!(key)
|
|
188
189
|
key = Fields::Internal.normalize_key(key)
|
|
@@ -236,7 +237,7 @@ module Julewire
|
|
|
236
237
|
end
|
|
237
238
|
|
|
238
239
|
def ensure_mutable_data!
|
|
239
|
-
@data = Fields::FieldSet.
|
|
240
|
+
@data = Fields::FieldSet.deep_dup_owned(@data) if @data.frozen?
|
|
240
241
|
end
|
|
241
242
|
|
|
242
243
|
def transformed_field_lineage(key, value)
|
|
@@ -553,7 +554,12 @@ module Julewire
|
|
|
553
554
|
def normalized_hash(value)
|
|
554
555
|
return empty_hash if value.is_a?(Hash) && value.empty?
|
|
555
556
|
|
|
556
|
-
|
|
557
|
+
if @input_owned
|
|
558
|
+
return Fields::Internal.frozen_deep_symbolize_owned_keys(value) if @freeze_sections
|
|
559
|
+
return value if value.is_a?(Hash)
|
|
560
|
+
|
|
561
|
+
return Fields::FieldSet.deep_symbolize_owned_keys(value)
|
|
562
|
+
end
|
|
557
563
|
return Fields::Internal.frozen_deep_symbolize_keys(value) if @freeze_sections
|
|
558
564
|
|
|
559
565
|
Fields::FieldSet.deep_symbolize_keys(value)
|
|
@@ -19,21 +19,20 @@ module Julewire
|
|
|
19
19
|
|
|
20
20
|
class << self
|
|
21
21
|
def from_normalized_hash(record, lineage: nil)
|
|
22
|
-
if record.is_a?(Hash)
|
|
23
|
-
lineage ||= Execution::Lineage.from_execution_hash(record[:execution])
|
|
24
|
-
record = record.merge(execution: Execution::Lineage.clean_lazy_relationship_hash(record[:execution]))
|
|
25
|
-
end
|
|
26
22
|
validate_normalized_hash!(record)
|
|
23
|
+
execution = record.fetch(:execution)
|
|
24
|
+
lineage ||= Execution::Lineage.from_execution_hash(execution)
|
|
25
|
+
record = record.merge(
|
|
26
|
+
execution: Execution::Lineage.clean_normalized_lazy_relationship_hash(execution)
|
|
27
|
+
)
|
|
27
28
|
new(snapshot_hash(record), lineage: lineage)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def from_owned_hash(record, lineage: nil, trust_frozen: false)
|
|
31
|
-
if record.is_a?(Hash)
|
|
32
|
-
lineage ||= Execution::Lineage.from_execution_hash(record.fetch(:execution))
|
|
33
|
-
execution = Execution::Lineage.clean_lazy_relationship_hash(record.fetch(:execution))
|
|
34
|
-
record = record.frozen? ? record.merge(execution: execution) : replace_execution(record, execution)
|
|
35
|
-
end
|
|
36
32
|
validate_normalized_hash!(record)
|
|
33
|
+
lineage ||= Execution::Lineage.from_execution_hash(record.fetch(:execution))
|
|
34
|
+
execution = Execution::Lineage.clean_normalized_lazy_relationship_hash(record.fetch(:execution))
|
|
35
|
+
record = record.frozen? ? record.merge(execution: execution) : replace_execution(record, execution)
|
|
37
36
|
new(Serialization::DeepFreeze.call(record, trust_frozen: trust_frozen), lineage: lineage)
|
|
38
37
|
end
|
|
39
38
|
|
|
@@ -70,8 +69,38 @@ module Julewire
|
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
def validate_symbol_keys!(record)
|
|
73
|
-
record
|
|
74
|
-
|
|
72
|
+
validate_value_symbol_keys!(record)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_value_symbol_keys!(root)
|
|
76
|
+
queue = [[root, 0]]
|
|
77
|
+
seen = {}.compare_by_identity
|
|
78
|
+
|
|
79
|
+
queue.each do |value, depth|
|
|
80
|
+
break if depth == NORMALIZATION_MAX_DEPTH
|
|
81
|
+
next unless mark_symbol_key_container(value, seen)
|
|
82
|
+
|
|
83
|
+
enqueue_symbol_key_children(queue, value, depth + 1)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def mark_symbol_key_container(value, seen)
|
|
88
|
+
return unless value.is_a?(Hash) || value.is_a?(Array)
|
|
89
|
+
return if seen.key?(value)
|
|
90
|
+
|
|
91
|
+
seen[value] = nil
|
|
92
|
+
value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def enqueue_symbol_key_children(queue, value, depth)
|
|
96
|
+
if value.is_a?(Hash)
|
|
97
|
+
value.each do |key, item|
|
|
98
|
+
raise TypeError, "record must not use string keys" if key.is_a?(String)
|
|
99
|
+
|
|
100
|
+
queue << [item, depth]
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
value.each { queue << [it, depth] }
|
|
75
104
|
end
|
|
76
105
|
end
|
|
77
106
|
|
|
@@ -126,7 +155,8 @@ module Julewire
|
|
|
126
155
|
def snapshot_hash(record)
|
|
127
156
|
Serialization::ValueCopy.call(
|
|
128
157
|
record,
|
|
129
|
-
freeze_values: true
|
|
158
|
+
freeze_values: true,
|
|
159
|
+
preserve_truncation_metadata: true
|
|
130
160
|
)
|
|
131
161
|
end
|
|
132
162
|
end
|
|
@@ -149,7 +179,7 @@ module Julewire
|
|
|
149
179
|
|
|
150
180
|
def each(&) = @data.each(&)
|
|
151
181
|
|
|
152
|
-
def to_h = Fields::FieldSet.
|
|
182
|
+
def to_h = Fields::FieldSet.deep_dup_owned(@data)
|
|
153
183
|
|
|
154
184
|
# @api internal
|
|
155
185
|
def serializable_data = @data
|
|
@@ -80,22 +80,14 @@ module Julewire
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
def emit_envelope(input:, context:, scope:, carry: {}, attributes: {}, neutral: {}, enforce_level: true
|
|
83
|
+
def emit_envelope(input:, context:, scope:, carry: {}, attributes: {}, neutral: {}, enforce_level: true,
|
|
84
|
+
owned: false)
|
|
84
85
|
reject_runtime_call_during_configure!(:emit_envelope)
|
|
85
86
|
state = runtime_state
|
|
86
87
|
return record_post_close_emit(state) if state.pipeline_closed
|
|
87
88
|
|
|
88
89
|
begin
|
|
89
|
-
record =
|
|
90
|
-
input,
|
|
91
|
-
context: envelope_hash(context),
|
|
92
|
-
attributes: envelope_hash(attributes),
|
|
93
|
-
neutral: envelope_hash(neutral),
|
|
94
|
-
carry: envelope_hash(carry),
|
|
95
|
-
scope: scope,
|
|
96
|
-
error_backtrace_lines: state.configuration.error_backtrace_lines,
|
|
97
|
-
invalid_severity_reporter: @invalid_severity_reporter
|
|
98
|
-
).to_record
|
|
90
|
+
record = envelope_draft(input, context, attributes, neutral, carry, scope, state, owned: owned).to_record
|
|
99
91
|
state.pipeline.emit_record(record, enforce_level: enforce_level)
|
|
100
92
|
rescue StandardError => e
|
|
101
93
|
notify_failure(e, state, action: :emit_envelope)
|
|
@@ -200,6 +192,33 @@ module Julewire
|
|
|
200
192
|
|
|
201
193
|
private
|
|
202
194
|
|
|
195
|
+
def envelope_draft(input, context, attributes, neutral, carry, scope, state, owned:)
|
|
196
|
+
if owned
|
|
197
|
+
Records::Draft.build_pipeline_owned(
|
|
198
|
+
input,
|
|
199
|
+
context: envelope_hash(context),
|
|
200
|
+
attributes: envelope_hash(attributes),
|
|
201
|
+
neutral: envelope_hash(neutral),
|
|
202
|
+
carry: envelope_hash(carry),
|
|
203
|
+
scope: scope,
|
|
204
|
+
error_backtrace_lines: state.configuration.error_backtrace_lines,
|
|
205
|
+
input_owned: true,
|
|
206
|
+
invalid_severity_reporter: @invalid_severity_reporter
|
|
207
|
+
)
|
|
208
|
+
else
|
|
209
|
+
Records::Draft.build(
|
|
210
|
+
input,
|
|
211
|
+
context: envelope_hash(context),
|
|
212
|
+
attributes: envelope_hash(attributes),
|
|
213
|
+
neutral: envelope_hash(neutral),
|
|
214
|
+
carry: envelope_hash(carry),
|
|
215
|
+
scope: scope,
|
|
216
|
+
error_backtrace_lines: state.configuration.error_backtrace_lines,
|
|
217
|
+
invalid_severity_reporter: @invalid_severity_reporter
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
203
222
|
def before_execution_boundary_call!(action)
|
|
204
223
|
reject_runtime_call_during_configure!(action)
|
|
205
224
|
end
|
|
@@ -49,6 +49,17 @@ module Julewire
|
|
|
49
49
|
transformed = @transform.call(value, key: key, path: path, original: @root, depth: depth)
|
|
50
50
|
transformed.equal?(CONTINUE) ? value : transformed
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
def truncation_metadata(fields)
|
|
54
|
+
TruncationMetadata.build(
|
|
55
|
+
fields,
|
|
56
|
+
key_style: :symbol,
|
|
57
|
+
max_array_items: @max_array_items,
|
|
58
|
+
max_depth: @max_depth,
|
|
59
|
+
max_hash_keys: @max_hash_keys,
|
|
60
|
+
max_string_bytes: @max_string_bytes
|
|
61
|
+
)
|
|
62
|
+
end
|
|
52
63
|
end
|
|
53
64
|
end
|
|
54
65
|
end
|