julewire-core 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/docs/advanced-configuration.md +66 -0
- data/docs/attribute-keys.md +74 -0
- data/docs/configuration.md +327 -0
- data/docs/context-and-propagation.md +353 -0
- data/docs/contracts.md +211 -0
- data/docs/development.md +49 -0
- data/docs/extensions-and-api.md +567 -0
- data/docs/health-schema.md +104 -0
- data/docs/instrumentation-cheatsheet.md +29 -0
- data/docs/internals.md +135 -0
- data/docs/outputs-and-lifecycle.md +206 -0
- data/docs/quickstart.md +133 -0
- data/docs/record-sources.md +17 -0
- data/docs/records-and-data-policy.md +230 -0
- data/docs/security-and-wire.md +45 -0
- data/docs/tail.md +91 -0
- data/exe/julewire +6 -0
- data/julewire-core.gemspec +41 -0
- data/lib/julewire/core/cli/doctor.rb +143 -0
- data/lib/julewire/core/cli/line_helpers.rb +77 -0
- data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
- data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
- data/lib/julewire/core/cli/log_formats.rb +123 -0
- data/lib/julewire/core/cli/tail.rb +153 -0
- data/lib/julewire/core/cli/transcode.rb +105 -0
- data/lib/julewire/core/cli.rb +73 -0
- data/lib/julewire/core/configuration.rb +99 -0
- data/lib/julewire/core/context_store.rb +384 -0
- data/lib/julewire/core/destinations/chaos_output.rb +91 -0
- data/lib/julewire/core/destinations/collection.rb +177 -0
- data/lib/julewire/core/destinations/definition.rb +125 -0
- data/lib/julewire/core/destinations/destination.rb +268 -0
- data/lib/julewire/core/destinations/registry.rb +81 -0
- data/lib/julewire/core/destinations/sink.rb +35 -0
- data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
- data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
- data/lib/julewire/core/destinations/write_step.rb +119 -0
- data/lib/julewire/core/destinations.rb +33 -0
- data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
- data/lib/julewire/core/diagnostics/doctor.rb +114 -0
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
- data/lib/julewire/core/diagnostics/health.rb +144 -0
- data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
- data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
- data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
- data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
- data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
- data/lib/julewire/core/diagnostics/tail.rb +168 -0
- data/lib/julewire/core/diagnostics.rb +8 -0
- data/lib/julewire/core/error.rb +7 -0
- data/lib/julewire/core/execution/boundary.rb +106 -0
- data/lib/julewire/core/execution/handle.rb +77 -0
- data/lib/julewire/core/execution/lineage.rb +192 -0
- data/lib/julewire/core/execution/measurement_handle.rb +28 -0
- data/lib/julewire/core/execution/no_current_error.rb +9 -0
- data/lib/julewire/core/execution/scope.rb +246 -0
- data/lib/julewire/core/execution/scope_fields.rb +76 -0
- data/lib/julewire/core/execution/scope_identity.rb +71 -0
- data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
- data/lib/julewire/core/execution/summary_state.rb +206 -0
- data/lib/julewire/core/execution/view.rb +56 -0
- data/lib/julewire/core/facade_methods.rb +181 -0
- data/lib/julewire/core/fields/attribute_keys.rb +54 -0
- data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
- data/lib/julewire/core/fields/bags.rb +123 -0
- data/lib/julewire/core/fields/carry_proxy.rb +22 -0
- data/lib/julewire/core/fields/context_proxy.rb +11 -0
- data/lib/julewire/core/fields/field_set.rb +78 -0
- data/lib/julewire/core/fields/field_stack.rb +269 -0
- data/lib/julewire/core/fields/internal/deletion.rb +68 -0
- data/lib/julewire/core/fields/internal.rb +87 -0
- data/lib/julewire/core/fields/lookup.rb +35 -0
- data/lib/julewire/core/fields/section_proxy.rb +88 -0
- data/lib/julewire/core/fields/stack_set.rb +69 -0
- data/lib/julewire/core/fields/static_labels.rb +43 -0
- data/lib/julewire/core/fields/summary_proxy.rb +62 -0
- data/lib/julewire/core/integration/configurable.rb +52 -0
- data/lib/julewire/core/integration/destination_health.rb +43 -0
- data/lib/julewire/core/integration/event_subscriber.rb +62 -0
- data/lib/julewire/core/integration/facade.rb +131 -0
- data/lib/julewire/core/integration/fork_hooks.rb +79 -0
- data/lib/julewire/core/integration/health.rb +41 -0
- data/lib/julewire/core/integration/ivar_state.rb +38 -0
- data/lib/julewire/core/integration/lifecycle.rb +22 -0
- data/lib/julewire/core/integration/scoped.rb +34 -0
- data/lib/julewire/core/integration/settings.rb +92 -0
- data/lib/julewire/core/integration/subscriber_install.rb +39 -0
- data/lib/julewire/core/integration/subscription.rb +29 -0
- data/lib/julewire/core/integration/values.rb +192 -0
- data/lib/julewire/core/lifecycle_error.rb +7 -0
- data/lib/julewire/core/local_storage.rb +91 -0
- data/lib/julewire/core/processing/level_threshold.rb +53 -0
- data/lib/julewire/core/processing/match.rb +74 -0
- data/lib/julewire/core/processing/pipeline.rb +360 -0
- data/lib/julewire/core/processing/processor_chain.rb +69 -0
- data/lib/julewire/core/processing/processor_registry.rb +115 -0
- data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
- data/lib/julewire/core/processing/record_field_transform.rb +124 -0
- data/lib/julewire/core/processing/sampling.rb +109 -0
- data/lib/julewire/core/processing.rb +41 -0
- data/lib/julewire/core/propagation/carrier.rb +93 -0
- data/lib/julewire/core/propagation.rb +50 -0
- data/lib/julewire/core/records/console_formatter.rb +24 -0
- data/lib/julewire/core/records/deconstruct.rb +19 -0
- data/lib/julewire/core/records/display_message.rb +166 -0
- data/lib/julewire/core/records/draft.rb +576 -0
- data/lib/julewire/core/records/formatter.rb +14 -0
- data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
- data/lib/julewire/core/records/metadata.rb +23 -0
- data/lib/julewire/core/records/public_projection.rb +51 -0
- data/lib/julewire/core/records/raw_input.rb +41 -0
- data/lib/julewire/core/records/record.rb +175 -0
- data/lib/julewire/core/records/severity.rb +44 -0
- data/lib/julewire/core/runtime.rb +515 -0
- data/lib/julewire/core/runtime_locator.rb +20 -0
- data/lib/julewire/core/runtime_registry.rb +48 -0
- data/lib/julewire/core/runtime_state.rb +39 -0
- data/lib/julewire/core/scheduling/deadline.rb +24 -0
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
- data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
- data/lib/julewire/core/sentinel.rb +18 -0
- data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
- data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
- data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
- data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
- data/lib/julewire/core/serialization/exception_shape.rb +88 -0
- data/lib/julewire/core/serialization/json_encoder.rb +69 -0
- data/lib/julewire/core/serialization/serializer.rb +233 -0
- data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
- data/lib/julewire/core/serialization/text_encoder.rb +147 -0
- data/lib/julewire/core/serialization/value_copy.rb +209 -0
- data/lib/julewire/core/serialization/value_traversal.rb +150 -0
- data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
- data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
- data/lib/julewire/core/testing/chaos/destination.rb +55 -0
- data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
- data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
- data/lib/julewire/core/testing/chaos.rb +80 -0
- data/lib/julewire/core/testing/contracts/component.rb +162 -0
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
- data/lib/julewire/core/testing/contracts/integration.rb +166 -0
- data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
- data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
- data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
- data/lib/julewire/core/testing/contracts/wire.rb +60 -0
- data/lib/julewire/core/testing/contracts.rb +24 -0
- data/lib/julewire/core/testing/coverage.rb +58 -0
- data/lib/julewire/core/testing/test_reports.rb +78 -0
- data/lib/julewire/core/testing.rb +122 -0
- data/lib/julewire/core/validation.rb +69 -0
- data/lib/julewire/core/version.rb +7 -0
- data/lib/julewire/core.rb +80 -0
- data/lib/julewire/error.rb +5 -0
- data/lib/julewire-core.rb +3 -0
- metadata +237 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Fields
|
|
6
|
+
# @api internal
|
|
7
|
+
# Immutable layers keep snapshots stable while each stack tracks only its
|
|
8
|
+
# current head and versioned read caches.
|
|
9
|
+
class FieldStack
|
|
10
|
+
EMPTY_HASH = {}.freeze
|
|
11
|
+
private_constant :EMPTY_HASH
|
|
12
|
+
|
|
13
|
+
class Layer
|
|
14
|
+
attr_reader :fields, :parent
|
|
15
|
+
|
|
16
|
+
def initialize(parent, fields, delete_paths: nil, clear_parent_deletes: true)
|
|
17
|
+
@parent = parent
|
|
18
|
+
@fields = fields
|
|
19
|
+
@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
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def snapshot
|
|
28
|
+
@snapshot ||= build_snapshot
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def value_for(key)
|
|
32
|
+
return @value_cache[key] if @value_cache&.key?(key)
|
|
33
|
+
|
|
34
|
+
value = if delete_paths_for_key?(key)
|
|
35
|
+
FieldSet.value_for(snapshot, key, default: MISSING)
|
|
36
|
+
else
|
|
37
|
+
field_value = FieldSet.value_for(@fields, key, default: MISSING)
|
|
38
|
+
field_value.equal?(MISSING) ? parent_value_for(key) : Fields::Internal.frozen_copy(field_value)
|
|
39
|
+
end
|
|
40
|
+
(@value_cache ||= {})[key] = value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def active_delete_paths
|
|
44
|
+
return @active_delete_paths if @active_delete_paths_computed
|
|
45
|
+
|
|
46
|
+
@active_delete_paths = build_active_delete_paths
|
|
47
|
+
@active_delete_paths_computed = true
|
|
48
|
+
@active_delete_paths
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def snapshot_cached?
|
|
52
|
+
!@snapshot.nil?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def delete_paths_for_snapshot
|
|
56
|
+
@clear_parent_deletes ? @delete_paths : active_delete_paths
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
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)
|
|
72
|
+
end
|
|
73
|
+
|
|
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)
|
|
79
|
+
end
|
|
80
|
+
|
|
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
|
|
87
|
+
|
|
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
|
|
102
|
+
|
|
103
|
+
def source_chain_base
|
|
104
|
+
source = self
|
|
105
|
+
source = source.parent until source.nil? || source.snapshot_cached?
|
|
106
|
+
source
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parent_value_for(key)
|
|
110
|
+
return MISSING unless @parent
|
|
111
|
+
|
|
112
|
+
@parent.value_for(key)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def delete_paths_for_key?(key)
|
|
116
|
+
active_delete_paths&.any? { it.first == key }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_active_delete_paths
|
|
120
|
+
paths = @parent&.active_delete_paths
|
|
121
|
+
paths = clear_active_delete_paths(paths) if paths && @clear_parent_deletes && !@fields.empty?
|
|
122
|
+
paths = append_delete_paths(paths) if @delete_paths
|
|
123
|
+
return unless paths
|
|
124
|
+
|
|
125
|
+
paths.empty? ? nil : paths
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def clear_active_delete_paths(paths)
|
|
129
|
+
paths = paths.dup
|
|
130
|
+
Fields::Internal.clear_delete_paths!(paths, @fields)
|
|
131
|
+
paths
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def append_delete_paths(paths)
|
|
135
|
+
paths ? paths + @delete_paths : @delete_paths
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
private_constant :Layer
|
|
139
|
+
|
|
140
|
+
def initialize(fields = {}, delete_paths: false, source: nil)
|
|
141
|
+
@source = source
|
|
142
|
+
@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?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def snapshot
|
|
151
|
+
return @snapshot if @snapshot_version == @version
|
|
152
|
+
|
|
153
|
+
@snapshot = @source ? @source.snapshot : EMPTY_HASH
|
|
154
|
+
@snapshot_version = @version
|
|
155
|
+
@snapshot
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def fork
|
|
159
|
+
self.class.new(delete_paths: @delete_paths_enabled, source: @source)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
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
|
+
|
|
172
|
+
value = source_value_for(key)
|
|
173
|
+
return default if value.equal?(MISSING)
|
|
174
|
+
|
|
175
|
+
(@value_cache ||= {})[key] = value
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def add(fields = nil, owned: false, **keyword_fields)
|
|
179
|
+
fields = field_input(fields, keyword_fields, owned: owned)
|
|
180
|
+
return unless fields.is_a?(Hash)
|
|
181
|
+
return if fields.empty?
|
|
182
|
+
|
|
183
|
+
fields = normalize_owned_keys(fields) if owned
|
|
184
|
+
@source = Layer.new(@source, fields, clear_parent_deletes: true)
|
|
185
|
+
invalidate_snapshot!
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def delete(path)
|
|
189
|
+
return if path.empty?
|
|
190
|
+
return unless @delete_paths_enabled
|
|
191
|
+
|
|
192
|
+
@source = Layer.new(@source, {}, delete_paths: [path], clear_parent_deletes: false)
|
|
193
|
+
invalidate_snapshot!
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def with(fields = nil, owned: false, **keyword_fields, &)
|
|
197
|
+
fields = field_input(fields, keyword_fields, owned: owned)
|
|
198
|
+
return yield unless fields.is_a?(Hash)
|
|
199
|
+
return yield if fields.empty?
|
|
200
|
+
|
|
201
|
+
fields = normalize_owned_keys(fields) if owned
|
|
202
|
+
with_layer(fields, &)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def without(path, &)
|
|
206
|
+
raise ArgumentError, "field path is required" if path.empty?
|
|
207
|
+
|
|
208
|
+
return yield unless @delete_paths_enabled
|
|
209
|
+
|
|
210
|
+
with_layer({}, delete_paths: [path], &)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def field_input(fields, keyword_fields, owned:)
|
|
216
|
+
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)
|
|
220
|
+
|
|
221
|
+
return keyword_fields
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
return FieldSet.deep_symbolize_keys(fields) if keyword_fields.empty?
|
|
225
|
+
|
|
226
|
+
FieldSet.coerce(fields, keyword_fields)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def with_layer(fields, delete_paths: nil)
|
|
230
|
+
previous_source = @source
|
|
231
|
+
@source = Layer.new(previous_source, fields, delete_paths: delete_paths, clear_parent_deletes: false)
|
|
232
|
+
invalidate_snapshot!
|
|
233
|
+
begin
|
|
234
|
+
yield
|
|
235
|
+
ensure
|
|
236
|
+
@source = previous_source
|
|
237
|
+
invalidate_snapshot!
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
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] }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def source_value_for(key)
|
|
248
|
+
return MISSING unless @source
|
|
249
|
+
|
|
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
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def invalidate_snapshot!
|
|
261
|
+
@version += 1
|
|
262
|
+
@snapshot = nil
|
|
263
|
+
@snapshot_version = nil
|
|
264
|
+
@value_cache = nil
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Fields
|
|
6
|
+
module Internal
|
|
7
|
+
module Deletion
|
|
8
|
+
class << self
|
|
9
|
+
def delete_path!(target, path)
|
|
10
|
+
normalized_path = normalize_path(path)
|
|
11
|
+
return target if normalized_path.empty?
|
|
12
|
+
|
|
13
|
+
deep_delete_path!(target, normalized_path)
|
|
14
|
+
target
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def apply_delete_paths!(target, paths)
|
|
18
|
+
paths.each { delete_path!(target, it) }
|
|
19
|
+
target
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def clear_delete_paths!(paths, fields)
|
|
23
|
+
additions = field_paths(fields)
|
|
24
|
+
paths.reject! do |path|
|
|
25
|
+
additions.any? { path_overlap?(path, it) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def normalize_path(path)
|
|
30
|
+
Array(path).flatten.filter_map { Internal.normalize_key(it) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def field_paths(fields, prefix = [])
|
|
36
|
+
return [] unless fields.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
fields.flat_map do |key, value|
|
|
39
|
+
path = prefix + [Internal.normalize_key(key)]
|
|
40
|
+
nested = value.is_a?(Hash) ? field_paths(value, path) : []
|
|
41
|
+
nested.empty? ? [path] : nested
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def path_overlap?(left, right)
|
|
46
|
+
shortest = [left.length, right.length].min
|
|
47
|
+
left.first(shortest) == right.first(shortest)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def deep_delete_path!(target, path)
|
|
51
|
+
return unless target.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
key = path.first
|
|
54
|
+
if path.one?
|
|
55
|
+
Internal.delete_key!(target, key)
|
|
56
|
+
return
|
|
57
|
+
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
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Fields
|
|
6
|
+
module Internal
|
|
7
|
+
EMPTY_ARRAY = [].freeze
|
|
8
|
+
EMPTY_HASH = {}.freeze
|
|
9
|
+
private_constant :EMPTY_ARRAY, :EMPTY_HASH
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def normalize_key(key)
|
|
13
|
+
key.is_a?(String) ? key.to_sym : key
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete_key!(target, key)
|
|
17
|
+
target.delete(normalize_key(key))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
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)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def deep_merge(left, right)
|
|
28
|
+
deep_merge!(FieldSet.deep_symbolize_keys(left), right)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def deep_merge!(target, fields)
|
|
32
|
+
merge_values!(target, fields) do |value, existing|
|
|
33
|
+
if existing.is_a?(Hash) && value.is_a?(Hash)
|
|
34
|
+
deep_merge!(existing, value)
|
|
35
|
+
else
|
|
36
|
+
FieldSet.deep_symbolize_keys(value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deep_merge_owned!(target, fields)
|
|
42
|
+
merge_values!(target, fields) do |value, existing|
|
|
43
|
+
if existing.is_a?(Hash) && value.is_a?(Hash)
|
|
44
|
+
deep_merge_owned!(existing, value)
|
|
45
|
+
else
|
|
46
|
+
value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def merge_owned!(target, fields)
|
|
52
|
+
merge_values!(target, fields) { |value, _existing| value }
|
|
53
|
+
end
|
|
54
|
+
|
|
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?
|
|
58
|
+
|
|
59
|
+
Serialization::ValueCopy.call(value, freeze_values: true, symbolize_keys: true)
|
|
60
|
+
end
|
|
61
|
+
|
|
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
|
+
def merge_values!(target, fields)
|
|
73
|
+
return target unless fields.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
fields.each do |key, value|
|
|
76
|
+
normalized_key = normalize_key(key)
|
|
77
|
+
existing = target[normalized_key]
|
|
78
|
+
target[normalized_key] = yield value, existing
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
target
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Fields
|
|
6
|
+
module Lookup
|
|
7
|
+
class << self
|
|
8
|
+
def value(source, key)
|
|
9
|
+
return unless source.respond_to?(:[])
|
|
10
|
+
|
|
11
|
+
direct = source[key]
|
|
12
|
+
return direct unless direct.nil?
|
|
13
|
+
|
|
14
|
+
alternate_key(source, key)
|
|
15
|
+
rescue StandardError
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def blank?(value)
|
|
20
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
21
|
+
end
|
|
22
|
+
|
|
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
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Fields
|
|
6
|
+
class SectionProxy
|
|
7
|
+
STORE_METHODS = Bags.app_write_sections.to_h do |section|
|
|
8
|
+
[
|
|
9
|
+
section,
|
|
10
|
+
{
|
|
11
|
+
add: :"add_#{section}",
|
|
12
|
+
hash: :"#{section}_hash",
|
|
13
|
+
value: :"#{section}_value",
|
|
14
|
+
with: :"with_#{section}"
|
|
15
|
+
}.freeze
|
|
16
|
+
]
|
|
17
|
+
end.freeze
|
|
18
|
+
private_constant :STORE_METHODS
|
|
19
|
+
|
|
20
|
+
def initialize(store, section)
|
|
21
|
+
@store = store
|
|
22
|
+
@section = section
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add(fields = nil, **keyword_fields)
|
|
26
|
+
add_fields(fields, keyword_fields) { add_section(it) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with(fields = nil, **keyword_fields, &)
|
|
30
|
+
raise ArgumentError, "block required" unless block_given?
|
|
31
|
+
|
|
32
|
+
with_fields(fields, keyword_fields) { with_section(it, &) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h = section_hash
|
|
36
|
+
|
|
37
|
+
def [](key) = nil_if_missing(section_value(key, default: MISSING))
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def coerce_fields(fields, keyword_fields)
|
|
42
|
+
FieldSet.coerce(fields, keyword_fields, invalid: :wrap)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def add_fields(fields, keyword_fields)
|
|
46
|
+
yield coerce_fields(fields, keyword_fields)
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_fields(fields, keyword_fields)
|
|
51
|
+
yield coerce_fields(fields, keyword_fields)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def nil_if_missing(value)
|
|
55
|
+
value.equal?(MISSING) ? nil : value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_section(fields)
|
|
59
|
+
call_store(:add, fields)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def with_section(fields, &)
|
|
63
|
+
call_store(:with, fields, &)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def section_hash
|
|
67
|
+
call_store(:hash)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def section_value(key, default:)
|
|
71
|
+
call_store(:value, key, default: default)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def call_store(action, ...)
|
|
75
|
+
@store.public_send(store_method(action), ...)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def store_method(action)
|
|
79
|
+
STORE_METHODS.fetch(section).fetch(action)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
attr_reader :section
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private_constant :SectionProxy
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Fields
|
|
6
|
+
class StackSet
|
|
7
|
+
EMPTY_HASH = {}.freeze
|
|
8
|
+
private_constant :EMPTY_HASH
|
|
9
|
+
|
|
10
|
+
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))]
|
|
14
|
+
end
|
|
15
|
+
new(**stacks)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def inherit_section?(section, inherit_attributes)
|
|
21
|
+
inherit_attributes || !%i[attributes neutral].include?(section)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inherited_stack(source, section, inherit)
|
|
25
|
+
inherit ? source.stack(section).fork : FieldStack.new
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(**sections)
|
|
30
|
+
@stacks = Bags.stack_sections.to_h do |section|
|
|
31
|
+
[section, field_stack(sections.fetch(section, EMPTY_HASH), section)]
|
|
32
|
+
end.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stack(section)
|
|
36
|
+
@stacks.fetch(section)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def snapshot(section)
|
|
40
|
+
stack(section).snapshot
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add(section, fields, owned: false)
|
|
44
|
+
stack(section).add(fields, owned: owned)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(section, path)
|
|
48
|
+
stack(section).delete(path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def with(section, fields, owned: false, &)
|
|
52
|
+
stack(section).with(fields, owned: owned, &)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def without(section, path, &)
|
|
56
|
+
stack(section).without(path, &)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def field_stack(value, section)
|
|
62
|
+
return value if value.is_a?(FieldStack)
|
|
63
|
+
|
|
64
|
+
FieldStack.new(value, delete_paths: Bags.delete_paths?(section))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Fields
|
|
6
|
+
class StaticLabels
|
|
7
|
+
def initialize
|
|
8
|
+
@fields = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(fields = nil, **keyword_fields)
|
|
12
|
+
FieldSet.merge!(@fields, FieldSet.coerce(fields, keyword_fields, invalid: :raise))
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clear
|
|
17
|
+
@fields.clear
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove(key)
|
|
22
|
+
Fields::Internal.delete_key!(@fields, key)
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
FieldSet.deep_dup(@fields)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def copy
|
|
31
|
+
self.class.new.tap do |copy|
|
|
32
|
+
copy.add(to_h)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def freeze
|
|
37
|
+
@fields.freeze
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|