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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 438690176aa21990767656cfc25cc35ebc89563611eb14f05875c463ef6e54f5
|
|
4
|
+
data.tar.gz: 6ce7ec058019b2ef04a84ec10f9f5fa22d73014b1449e272b8727fc1cbd9d45a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b965bae8da19cd50a0acf2df7d2005bb421c4b256fc286c7cfb89d2c58b034eefcdaafded7bfacf02243948f63977e4d390cde723f4f1c3f34f3b646dc9ed96d
|
|
7
|
+
data.tar.gz: a9623120a2bb1c1af5a5c2c877bfcdc74a71225f6826781348fa479c223dc167ff838fa60b8b0759d829307c1f4a180497746a132b7ab65de94d2f7ceb2f2bb1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
## 1.0.1 - 2026-06-25
|
|
4
|
+
|
|
5
|
+
- Harden bounded traversal, ingress copying, and carrier extraction against
|
|
6
|
+
noisy or hostile record shapes.
|
|
7
|
+
- Keep normalized record constructors strict: symbol-key contracts validate
|
|
8
|
+
instead of quietly normalizing pipeline-owned data.
|
|
9
|
+
- Default carrier extraction to the byte cap, report extraction status to
|
|
10
|
+
integrations, reserve truncation metadata keys at field ingress, and keep
|
|
11
|
+
custom normalization limits out of the thread-local copier pool.
|
|
12
|
+
- Keep output writes independent from flush/close lifecycle locking.
|
|
13
|
+
|
|
3
14
|
## 1.0.0 - 2026-06-21
|
|
4
15
|
|
|
5
16
|
- Initial release: execution-scoped structured logging, propagation, bounded
|
|
@@ -13,7 +13,8 @@ is unsupported. Call `Julewire.configure` again.
|
|
|
13
13
|
User-supplied destination formatter, encoder, output, and processor instances
|
|
14
14
|
are shared by reference. Processors, formatters, and encoders must be stateless
|
|
15
15
|
or otherwise reentrant; core does not synchronize them. Direct outputs are
|
|
16
|
-
wrapped with a per-destination mutex
|
|
16
|
+
wrapped with a per-destination write mutex; `flush` may overlap writes, while
|
|
17
|
+
terminal `close` is serialized with writes.
|
|
17
18
|
|
|
18
19
|
When a reconfigure reuses the same output or destination object, core keeps that
|
|
19
20
|
resource open for the new active pipeline and skips teardown through the old
|
|
@@ -241,9 +241,9 @@ Julewire::Core::Propagation.restore(envelope, link_executions: true) do
|
|
|
241
241
|
end
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
Restoring an envelope is an explicit trust decision. Core validates shape
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
Restoring an envelope is an explicit trust decision. Core validates shape and
|
|
245
|
+
caps inbound carrier bytes by default, but core does not authenticate who wrote
|
|
246
|
+
the carrier. At untrusted boundaries, extract only the carrier fields you
|
|
247
247
|
intentionally accept, apply any application spoofing/filter policy first, then
|
|
248
248
|
call `Carrier.restore` on that filtered carrier map.
|
|
249
249
|
|
|
@@ -269,7 +269,9 @@ Julewire::Core::Propagation::Carrier.restore(trusted) do
|
|
|
269
269
|
end
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
Inbound extraction and restore default to `Carrier::DEFAULT_MAX_BYTES`; pass
|
|
273
|
+
`max_bytes: nil` only for a trusted unbounded carrier. Use `max_bytes:` on
|
|
274
|
+
injection when the carrier target has stricter size constraints:
|
|
273
275
|
|
|
274
276
|
```ruby
|
|
275
277
|
headers = Julewire::Core::Propagation::Carrier.inject({}, max_bytes: 8 * 1024)
|
data/docs/contracts.md
CHANGED
|
@@ -190,7 +190,8 @@ different Ruby isolation boundary:
|
|
|
190
190
|
- `Julewire::Core::Execution::ScopeSnapshot` for detached execution scope transfer.
|
|
191
191
|
- Parent-runtime hooks: `emit_envelope`, `emit_summary_record`, and `flush`.
|
|
192
192
|
`emit_envelope` accepts detached input, context, carry, attributes, neutral,
|
|
193
|
-
scope snapshot, and
|
|
193
|
+
scope snapshot, `enforce_level:`, and `owned:`. Bridges pass `owned: true`
|
|
194
|
+
only for envelopes that came from Julewire's own wire format.
|
|
194
195
|
|
|
195
196
|
Bridge runtimes may expose `emit_without_level` when integration code inside
|
|
196
197
|
the bridge has already applied its own level gate. Parent runtime labels,
|
data/docs/extensions-and-api.md
CHANGED
|
@@ -300,11 +300,11 @@ health
|
|
|
300
300
|
```
|
|
301
301
|
|
|
302
302
|
Output lifecycle hooks are sync and local. Runtime timeouts provide one carried
|
|
303
|
-
deadline while core walks destinations
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
destinations own async drain,
|
|
307
|
-
timeout-aware shutdown.
|
|
303
|
+
deadline while core walks destinations. Plain outputs receive the timeout only
|
|
304
|
+
when their lifecycle method accepts `timeout:` or `**kwargs`; core still cannot
|
|
305
|
+
interrupt blocking raw output code. After the first attempted resource,
|
|
306
|
+
exhausted deadlines skip later resources. Custom destinations own async drain,
|
|
307
|
+
retry, reopen targets, rotation, and timeout-aware shutdown.
|
|
308
308
|
|
|
309
309
|
Plain output writes are successful when `write` returns without raising and does
|
|
310
310
|
not return `false`. A plain `false` is treated as a rejected write. A rejection
|
|
@@ -363,6 +363,8 @@ an immutable `Record` for formatters and destinations. `Record` is not a raw
|
|
|
363
363
|
input builder; it is the read-only destination boundary. Use
|
|
364
364
|
`Record.from_normalized_hash` only when an extension already owns a complete
|
|
365
365
|
symbol-key normalized record hash and needs the immutable destination shape.
|
|
366
|
+
That path validates the strict internal contract; it does not clean up
|
|
367
|
+
JSON-style or user-input hashes. Use `RecordDraft.build` at raw boundaries.
|
|
366
368
|
|
|
367
369
|
## Public Facade
|
|
368
370
|
|
|
@@ -564,4 +566,5 @@ runtimes fail when called, so application code should not replace it casually.
|
|
|
564
566
|
|
|
565
567
|
Core exposes the bridge SPI runtime envelope hook used by bridge code. The hook
|
|
566
568
|
accepts detached input, context, attributes, carry, neutral data, and a scope
|
|
567
|
-
snapshot, then routes them through the active pipeline.
|
|
569
|
+
snapshot, then routes them through the active pipeline. Bridge code may pass
|
|
570
|
+
`owned: true` only for data decoded from Julewire-owned wire formats.
|
data/docs/internals.md
CHANGED
|
@@ -97,11 +97,11 @@ own schedulers because worker ractors cannot share the main scheduler object.
|
|
|
97
97
|
|
|
98
98
|
## Remote Envelope Hook
|
|
99
99
|
|
|
100
|
-
Core keeps `Runtime#emit_envelope(input:, context:, attributes:, carry:, neutral:, scope:, enforce_level:)`
|
|
100
|
+
Core keeps `Runtime#emit_envelope(input:, context:, attributes:, carry:, neutral:, scope:, enforce_level:, owned:)`
|
|
101
101
|
for bridge code. The bridge reconstructs the scope snapshot, and core rebuilds
|
|
102
102
|
a normal record from input, context, attributes, carry, neutral, and that
|
|
103
|
-
snapshot before emitting through the active pipeline.
|
|
104
|
-
application API.
|
|
103
|
+
snapshot before emitting through the active pipeline. Bridges pass `owned: true`
|
|
104
|
+
only for Julewire-owned wire data. It is not a public application API.
|
|
105
105
|
|
|
106
106
|
## Test Seams
|
|
107
107
|
|
|
@@ -74,8 +74,11 @@ for `$stdout`, caller-owned loggers, and shared objects.
|
|
|
74
74
|
|
|
75
75
|
## Synchronous Output
|
|
76
76
|
|
|
77
|
-
Core wraps direct
|
|
78
|
-
individual encoded records, but a slow sink blocks the emitting
|
|
77
|
+
Core wraps direct output writes with one mutex. Concurrent emitters do not
|
|
78
|
+
interleave individual encoded records, but a slow sink blocks the emitting
|
|
79
|
+
thread. `flush` is lifecycle-only and may overlap writes so slow flushes do not
|
|
80
|
+
stall the emit path; direct outputs should be IO-like or internally safe for
|
|
81
|
+
flush/write overlap. `close` is terminal and serialized with writes.
|
|
79
82
|
|
|
80
83
|
A synchronous output failure is contained. The destination records output
|
|
81
84
|
failure counters and calls `on_failure`. Core does not retry, back off, or
|
|
@@ -181,8 +181,8 @@ Put redaction in processors or a separate policy gem before formatting.
|
|
|
181
181
|
|
|
182
182
|
Truncated containers, containers pruned by `max_depth`, and circular container
|
|
183
183
|
references get `_julewire_truncation` metadata. Serializer keys beginning with
|
|
184
|
-
`_julewire_` are reserved
|
|
185
|
-
|
|
184
|
+
`_julewire_` are reserved for core metadata; public field ingress rejects the
|
|
185
|
+
truncation marker as a user key. Public Julewire record contracts use symbol keys
|
|
186
186
|
internally. Payloads should also use one JSON field name per value; mixed key
|
|
187
187
|
types that stringify to the same JSON field are outside the serializer contract.
|
|
188
188
|
Long strings use a `...[Truncated]` suffix. User data that already contains that
|
|
@@ -72,6 +72,7 @@ module Julewire
|
|
|
72
72
|
current_field_hash(:neutral)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# SectionProxy dispatches these dynamically from the public field readers.
|
|
75
76
|
def context_value(key, default:)
|
|
76
77
|
current_field_stack(:context).value_for(key, default: default)
|
|
77
78
|
end
|
|
@@ -139,21 +140,25 @@ module Julewire
|
|
|
139
140
|
end
|
|
140
141
|
end
|
|
141
142
|
|
|
142
|
-
def with_propagation(context: {}, carry: {}, execution: {}, link_executions: false, &)
|
|
143
|
+
def with_propagation(context: {}, carry: {}, execution: {}, link_executions: false, owned: false, &)
|
|
143
144
|
scope = current_scope
|
|
144
|
-
execution =
|
|
145
|
+
execution = if owned
|
|
146
|
+
Fields::FieldSet.deep_symbolize_owned_keys(execution)
|
|
147
|
+
else
|
|
148
|
+
Fields::FieldSet.deep_symbolize_keys(execution)
|
|
149
|
+
end
|
|
145
150
|
@execution_overlays.push(execution)
|
|
146
151
|
@execution_lineage_overlays.push(link_executions ? Execution::Lineage.from_execution_hash(execution) : nil)
|
|
147
152
|
invalidate_propagation_cache!
|
|
148
153
|
|
|
149
154
|
begin
|
|
150
155
|
if scope
|
|
151
|
-
scope.with_carry(carry) do
|
|
152
|
-
scope.with_context(context, &)
|
|
156
|
+
scope.with_carry(carry, owned: owned) do
|
|
157
|
+
scope.with_context(context, owned: owned, &)
|
|
153
158
|
end
|
|
154
159
|
else
|
|
155
|
-
@ambient_fields.with(:carry, carry) do
|
|
156
|
-
@ambient_fields.with(:context, context, &)
|
|
160
|
+
@ambient_fields.with(:carry, carry, owned: owned) do
|
|
161
|
+
@ambient_fields.with(:context, context, owned: owned, &)
|
|
157
162
|
end
|
|
158
163
|
end
|
|
159
164
|
ensure
|
|
@@ -242,9 +242,9 @@ module Julewire
|
|
|
242
242
|
call_output_lifecycle_safely(method_name, timeout)
|
|
243
243
|
end
|
|
244
244
|
|
|
245
|
-
def call_output_lifecycle_safely(method_name,
|
|
246
|
-
#
|
|
247
|
-
result = @output.public_send(method_name)
|
|
245
|
+
def call_output_lifecycle_safely(method_name, timeout)
|
|
246
|
+
# Sink.wrap centralizes timeout-aware lifecycle dispatch for every output.
|
|
247
|
+
result = @output.public_send(method_name, timeout: timeout)
|
|
248
248
|
clear_degradation if method_name == :flush && result != false
|
|
249
249
|
result
|
|
250
250
|
rescue StandardError => e
|
|
@@ -4,16 +4,23 @@ module Julewire
|
|
|
4
4
|
module Core
|
|
5
5
|
module Destinations
|
|
6
6
|
class SynchronizedOutput
|
|
7
|
+
TIMEOUT_PARAMETER_TYPES = %i[key keyreq].freeze
|
|
8
|
+
private_constant :TIMEOUT_PARAMETER_TYPES
|
|
9
|
+
|
|
7
10
|
def initialize(output, close_output: false)
|
|
8
11
|
Sink.validate_writeable!(output)
|
|
9
12
|
@output = output
|
|
10
13
|
@close_output = close_output
|
|
11
14
|
@mutex = Mutex.new
|
|
15
|
+
@lifecycle_mutex = Mutex.new
|
|
16
|
+
@lifecycle = lifecycle_methods
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def after_fork!
|
|
15
20
|
@mutex = Mutex.new
|
|
21
|
+
@lifecycle_mutex = Mutex.new
|
|
16
22
|
@output.after_fork! if @output.respond_to?(:after_fork!)
|
|
23
|
+
@lifecycle = lifecycle_methods
|
|
17
24
|
self
|
|
18
25
|
end
|
|
19
26
|
|
|
@@ -22,27 +29,36 @@ module Julewire
|
|
|
22
29
|
def resource_identity = @output
|
|
23
30
|
|
|
24
31
|
def write(value)
|
|
25
|
-
@mutex.synchronize
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
return false if output_closed?
|
|
34
|
+
|
|
35
|
+
@output.write(value)
|
|
36
|
+
end
|
|
26
37
|
end
|
|
27
38
|
|
|
28
|
-
def flush
|
|
29
|
-
@
|
|
30
|
-
|
|
39
|
+
def flush(timeout: nil)
|
|
40
|
+
@lifecycle_mutex.synchronize do
|
|
41
|
+
lifecycle = @lifecycle[:flush]
|
|
42
|
+
return true unless lifecycle
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
call_lifecycle(:flush, lifecycle, timeout: timeout) != false
|
|
33
45
|
end
|
|
34
46
|
end
|
|
35
47
|
|
|
36
|
-
def close
|
|
37
|
-
@
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
def close(timeout: nil)
|
|
49
|
+
@lifecycle_mutex.synchronize do
|
|
50
|
+
# Close is terminal: lifecycle calls stay serialized, and the write mutex
|
|
51
|
+
# keeps the underlying output from being closed while a write is in flight.
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
return true if output_closed?
|
|
54
|
+
|
|
55
|
+
result = if @close_output && @lifecycle[:close]
|
|
56
|
+
call_lifecycle(:close, @lifecycle.fetch(:close), timeout: timeout)
|
|
57
|
+
elsif @lifecycle[:flush]
|
|
58
|
+
call_lifecycle(:flush, @lifecycle.fetch(:flush), timeout: timeout)
|
|
59
|
+
end
|
|
60
|
+
result != false
|
|
61
|
+
end
|
|
46
62
|
end
|
|
47
63
|
end
|
|
48
64
|
|
|
@@ -51,6 +67,26 @@ module Julewire
|
|
|
51
67
|
def output_closed?
|
|
52
68
|
@output.respond_to?(:closed?) ? @output.closed? : false
|
|
53
69
|
end
|
|
70
|
+
|
|
71
|
+
def call_lifecycle(name, lifecycle, timeout:)
|
|
72
|
+
return @output.public_send(name, timeout: timeout) if lifecycle.fetch(:timeout)
|
|
73
|
+
|
|
74
|
+
@output.public_send(name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def lifecycle_methods
|
|
78
|
+
%i[flush close].each_with_object({}) do |name, methods|
|
|
79
|
+
next unless @output.respond_to?(name)
|
|
80
|
+
|
|
81
|
+
method = @output.method(name)
|
|
82
|
+
methods[name] = { timeout: accepts_timeout_keyword?(method) }.freeze
|
|
83
|
+
end.freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def accepts_timeout_keyword?(method)
|
|
87
|
+
method.parameters.any? { |type, name| TIMEOUT_PARAMETER_TYPES.include?(type) && name == :timeout } ||
|
|
88
|
+
method.parameters.any? { |type, _name| type == :keyrest }
|
|
89
|
+
end
|
|
54
90
|
end
|
|
55
91
|
end
|
|
56
92
|
end
|
|
@@ -52,25 +52,25 @@ module Julewire
|
|
|
52
52
|
|
|
53
53
|
def destination_info(destinations)
|
|
54
54
|
destinations.transform_values do |destination|
|
|
55
|
-
|
|
56
|
-
counts: destination[:counts],
|
|
57
|
-
last_failure: destination[:last_failure],
|
|
58
|
-
last_loss: destination[:last_loss],
|
|
59
|
-
status: destination[:status]
|
|
60
|
-
}.compact
|
|
55
|
+
component_info(destination, include_loss: true)
|
|
61
56
|
end
|
|
62
57
|
end
|
|
63
58
|
|
|
64
59
|
def integration_info(integrations)
|
|
65
60
|
integrations.transform_values do |integration|
|
|
66
|
-
|
|
67
|
-
counts: integration[:counts],
|
|
68
|
-
last_failure: integration[:last_failure],
|
|
69
|
-
status: integration[:status]
|
|
70
|
-
}.compact
|
|
61
|
+
component_info(integration)
|
|
71
62
|
end
|
|
72
63
|
end
|
|
73
64
|
|
|
65
|
+
def component_info(component, include_loss: false)
|
|
66
|
+
{
|
|
67
|
+
counts: component[:counts],
|
|
68
|
+
last_failure: component[:last_failure],
|
|
69
|
+
last_loss: include_loss ? component[:last_loss] : nil,
|
|
70
|
+
status: component[:status]
|
|
71
|
+
}.compact
|
|
72
|
+
end
|
|
73
|
+
|
|
74
74
|
def warnings(health)
|
|
75
75
|
[].tap do |items|
|
|
76
76
|
items << warning(:runtime_closed, "runtime is closed") if health.fetch(:closed)
|
|
@@ -16,7 +16,9 @@ module Julewire
|
|
|
16
16
|
integration: metadata[:integration],
|
|
17
17
|
output_class: metadata[:output_class],
|
|
18
18
|
phase: metadata[:phase],
|
|
19
|
-
|
|
19
|
+
reason: metadata[:reason],
|
|
20
|
+
record: record_metadata(metadata[:record_metadata]),
|
|
21
|
+
status: metadata[:status]
|
|
20
22
|
}.compact.freeze
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -33,6 +33,8 @@ module Julewire
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def finish(reason: :closed, fields: {}, attributes: {}, error: nil, severity: nil)
|
|
36
|
+
# Finishing is one-shot: retrying after partial summary mutation can
|
|
37
|
+
# duplicate completion data, so failures are reported and stop here.
|
|
36
38
|
return false unless mark_finished
|
|
37
39
|
|
|
38
40
|
add_completion_attributes(reason)
|
|
@@ -15,12 +15,12 @@ module Julewire
|
|
|
15
15
|
clean_hash(execution, RELATIONSHIP_KEYS)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
|
|
18
|
+
def clean_normalized_lazy_relationship_hash(execution)
|
|
19
|
+
clean_normalized_hash(execution, LAZY_RELATIONSHIP_KEYS)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
clean_hash(execution,
|
|
22
|
+
def clean_owned_execution_hash(execution)
|
|
23
|
+
clean_hash!(execution, RELATIONSHIP_KEYS)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def from_execution_hash(execution)
|
|
@@ -50,6 +50,11 @@ module Julewire
|
|
|
50
50
|
clean_hash!(copy, keys)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def clean_normalized_hash(execution, keys)
|
|
54
|
+
copy = execution.is_a?(Hash) ? execution.dup : {}
|
|
55
|
+
clean_hash!(copy, keys)
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
def clean_hash!(copy, keys)
|
|
54
59
|
keys.each do |key|
|
|
55
60
|
Fields::Internal.delete_key!(copy, key)
|
|
@@ -182,8 +187,7 @@ module Julewire
|
|
|
182
187
|
|
|
183
188
|
def frozen_hash_copy(value)
|
|
184
189
|
value.each_with_object({}) do |(key, field_value), copy|
|
|
185
|
-
copy[
|
|
186
|
-
Serialization::ValueCopy.call(field_value, freeze_values: true)
|
|
190
|
+
copy[key] = Serialization::ValueCopy.call(field_value, freeze_values: true)
|
|
187
191
|
end
|
|
188
192
|
end
|
|
189
193
|
end
|
|
@@ -109,12 +109,12 @@ module Julewire
|
|
|
109
109
|
@fields.with(section, fields, owned: owned, &)
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
def with_context(fields, &)
|
|
113
|
-
|
|
112
|
+
def with_context(fields = nil, owned: false, **keyword_fields, &)
|
|
113
|
+
@fields.with(:context, fields, owned: owned, **keyword_fields, &)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
def with_carry(fields, &)
|
|
117
|
-
|
|
116
|
+
def with_carry(fields = nil, owned: false, **keyword_fields, &)
|
|
117
|
+
@fields.with(:carry, fields, owned: owned, **keyword_fields, &)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
def without_carry(path, &)
|
|
@@ -55,8 +55,8 @@ module Julewire
|
|
|
55
55
|
@stacks.delete(section, path)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
def with(section, fields, owned: false, &)
|
|
59
|
-
@stacks.with(section, fields, owned: owned, &)
|
|
58
|
+
def with(section, fields = nil, owned: false, **keyword_fields, &)
|
|
59
|
+
@stacks.with(section, fields, owned: owned, **keyword_fields, &)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def without(section, path, &)
|
|
@@ -105,7 +105,7 @@ module Julewire
|
|
|
105
105
|
envelope = Core::Propagation.capture_local
|
|
106
106
|
Fiber.new(**) do |*args|
|
|
107
107
|
with_cleared_configure_guard do
|
|
108
|
-
Core::Propagation.restore(envelope) { yield(*args) }
|
|
108
|
+
Core::Propagation.restore(envelope, owned: true) { yield(*args) }
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
end
|
|
@@ -132,7 +132,7 @@ module Julewire
|
|
|
132
132
|
envelope = Core::Propagation.capture_local
|
|
133
133
|
Thread.new(*) do |*thread_args|
|
|
134
134
|
with_cleared_configure_guard do
|
|
135
|
-
Core::Propagation.restore(envelope) { yield(*thread_args) }
|
|
135
|
+
Core::Propagation.restore(envelope, owned: true) { yield(*thread_args) }
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
end
|
|
@@ -32,17 +32,19 @@ module Julewire
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def deep_dup(value)
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
deep_dup_with(value, preserve_truncation_metadata: false)
|
|
36
|
+
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
def deep_dup_owned(value)
|
|
39
|
+
deep_dup_with(value, preserve_truncation_metadata: true)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def deep_symbolize_keys(value)
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
deep_symbolize_keys_with(value, preserve_truncation_metadata: false)
|
|
44
|
+
end
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
def deep_symbolize_owned_keys(value)
|
|
47
|
+
deep_symbolize_keys_with(value, preserve_truncation_metadata: true)
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def frozen_copy(value)
|
|
@@ -60,6 +62,30 @@ module Julewire
|
|
|
60
62
|
|
|
61
63
|
private
|
|
62
64
|
|
|
65
|
+
def deep_dup_with(value, preserve_truncation_metadata:)
|
|
66
|
+
return {} if value.is_a?(Hash) && value.empty?
|
|
67
|
+
return [] if value.is_a?(Array) && value.empty?
|
|
68
|
+
|
|
69
|
+
Serialization::ValueCopy.call(
|
|
70
|
+
value,
|
|
71
|
+
preserve_truncation_metadata: preserve_truncation_metadata
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def deep_symbolize_keys_with(value, preserve_truncation_metadata:)
|
|
76
|
+
return {} if value.is_a?(Hash) && value.empty?
|
|
77
|
+
return [] if value.is_a?(Array) && value.empty?
|
|
78
|
+
|
|
79
|
+
Serialization::ValueCopy.call(
|
|
80
|
+
value,
|
|
81
|
+
max_array_items: Serialization::Serializer::DEFAULT_MAX_ARRAY_ITEMS,
|
|
82
|
+
max_hash_keys: Serialization::Serializer::DEFAULT_MAX_HASH_KEYS,
|
|
83
|
+
max_string_bytes: Serialization::Serializer::DEFAULT_MAX_STRING_BYTES,
|
|
84
|
+
preserve_truncation_metadata: preserve_truncation_metadata,
|
|
85
|
+
symbolize_keys: true
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
63
89
|
def coerce_fields!(target, fields, invalid:)
|
|
64
90
|
if fields.is_a?(Hash)
|
|
65
91
|
merge!(target, fields)
|