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.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. 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