julewire-core 1.0.1 → 1.1.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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/docs/configuration.md +14 -7
  4. data/docs/context-and-propagation.md +20 -0
  5. data/docs/contracts.md +23 -49
  6. data/docs/extensions-and-api.md +78 -82
  7. data/docs/health-schema.md +12 -2
  8. data/docs/internals.md +21 -20
  9. data/docs/outputs-and-lifecycle.md +10 -6
  10. data/docs/records-and-data-policy.md +36 -5
  11. data/lib/julewire/core/cli/doctor.rb +20 -16
  12. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  13. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  14. data/lib/julewire/core/cli/log_formats.rb +9 -9
  15. data/lib/julewire/core/cli/tail.rb +19 -14
  16. data/lib/julewire/core/cli/transcode.rb +4 -5
  17. data/lib/julewire/core/cli.rb +1 -1
  18. data/lib/julewire/core/configuration.rb +1 -1
  19. data/lib/julewire/core/context_store.rb +51 -87
  20. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  21. data/lib/julewire/core/destinations/collection.rb +8 -16
  22. data/lib/julewire/core/destinations/definition.rb +1 -9
  23. data/lib/julewire/core/destinations/destination.rb +13 -63
  24. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  25. data/lib/julewire/core/destinations/registry.rb +1 -1
  26. data/lib/julewire/core/destinations/sink.rb +0 -1
  27. data/lib/julewire/core/destinations/synchronized_output.rb +14 -13
  28. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  29. data/lib/julewire/core/destinations/write_step.rb +5 -5
  30. data/lib/julewire/core/destinations.rb +0 -7
  31. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  32. data/lib/julewire/core/diagnostics/doctor.rb +16 -9
  33. data/lib/julewire/core/diagnostics/failure_snapshot.rb +6 -2
  34. data/lib/julewire/core/diagnostics/health.rb +41 -57
  35. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  36. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  37. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  38. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  39. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  40. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  41. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  42. data/lib/julewire/core/execution/boundary.rb +2 -8
  43. data/lib/julewire/core/execution/handle.rb +12 -17
  44. data/lib/julewire/core/execution/lineage.rb +16 -56
  45. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  46. data/lib/julewire/core/execution/scope.rb +15 -20
  47. data/lib/julewire/core/execution/scope_fields.rb +3 -12
  48. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  49. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  50. data/lib/julewire/core/execution/summary_state.rb +16 -19
  51. data/lib/julewire/core/execution/view.rb +8 -17
  52. data/lib/julewire/core/facade_methods.rb +67 -65
  53. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  54. data/lib/julewire/core/fields/bags.rb +23 -27
  55. data/lib/julewire/core/fields/field_set.rb +29 -19
  56. data/lib/julewire/core/fields/field_stack.rb +68 -134
  57. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  58. data/lib/julewire/core/fields/internal.rb +19 -38
  59. data/lib/julewire/core/fields/lookup.rb +9 -13
  60. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  61. data/lib/julewire/core/fields/stack_set.rb +7 -12
  62. data/lib/julewire/core/fields/static_labels.rb +1 -1
  63. data/lib/julewire/core/integration/configurable.rb +4 -3
  64. data/lib/julewire/core/integration/destination_health.rb +17 -7
  65. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  66. data/lib/julewire/core/integration/facade.rb +12 -15
  67. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  68. data/lib/julewire/core/integration/health.rb +0 -3
  69. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  70. data/lib/julewire/core/integration/protocol.rb +20 -0
  71. data/lib/julewire/core/integration/settings.rb +4 -4
  72. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  73. data/lib/julewire/core/integration/subscription.rb +3 -2
  74. data/lib/julewire/core/integration/values.rb +0 -17
  75. data/lib/julewire/core/local_storage.rb +3 -19
  76. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  77. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  78. data/lib/julewire/core/processing/match.rb +6 -4
  79. data/lib/julewire/core/processing/pipeline.rb +46 -40
  80. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  81. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  82. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  83. data/lib/julewire/core/processing/sampling.rb +1 -3
  84. data/lib/julewire/core/processing.rb +2 -2
  85. data/lib/julewire/core/propagation/carrier.rb +14 -14
  86. data/lib/julewire/core/propagation.rb +15 -2
  87. data/lib/julewire/core/records/build_input.rb +75 -0
  88. data/lib/julewire/core/records/console_formatter.rb +6 -6
  89. data/lib/julewire/core/records/deconstruct.rb +1 -1
  90. data/lib/julewire/core/records/display_message.rb +2 -4
  91. data/lib/julewire/core/records/draft.rb +125 -262
  92. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  93. data/lib/julewire/core/records/metadata.rb +4 -3
  94. data/lib/julewire/core/records/public_projection.rb +2 -2
  95. data/lib/julewire/core/records/raw_input.rb +1 -1
  96. data/lib/julewire/core/records/record.rb +19 -37
  97. data/lib/julewire/core/records/severity.rb +7 -15
  98. data/lib/julewire/core/runtime.rb +67 -71
  99. data/lib/julewire/core/runtime_registry.rb +20 -15
  100. data/lib/julewire/core/runtime_state.rb +5 -3
  101. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  102. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  103. data/lib/julewire/core/serialization/bounded_transform.rb +2 -12
  104. data/lib/julewire/core/serialization/bounded_traversal.rb +49 -66
  105. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  106. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  107. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  108. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  109. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  110. data/lib/julewire/core/serialization/serializer.rb +23 -50
  111. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  112. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  113. data/lib/julewire/core/serialization/truncation_metadata.rb +63 -48
  114. data/lib/julewire/core/serialization/value_copy.rb +60 -227
  115. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  116. data/lib/julewire/core/testing.rb +6 -56
  117. data/lib/julewire/core/validation.rb +2 -3
  118. data/lib/julewire/core/version.rb +1 -1
  119. data/lib/julewire/core.rb +2 -7
  120. metadata +6 -19
  121. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  122. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  123. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  124. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  125. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  126. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  127. data/lib/julewire/core/testing/chaos.rb +0 -80
  128. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  129. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  130. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  131. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  132. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  133. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  134. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  135. data/lib/julewire/core/testing/contracts.rb +0 -24
  136. data/lib/julewire/core/testing/coverage.rb +0 -58
  137. data/lib/julewire/core/testing/test_reports.rb +0 -78
@@ -26,35 +26,31 @@ module Julewire
26
26
  source
27
27
  ].freeze
28
28
 
29
- class << self
30
- private
31
-
32
- def define(name, **capabilities)
33
- Definition.new(
34
- name: name,
35
- record_hash: true,
36
- transform_container: true,
37
- app_write: false,
38
- integration_write: false,
39
- propagate: false,
40
- emit_by_default: true,
41
- delete_paths: false,
42
- stack: false,
43
- **capabilities
44
- )
45
- end
29
+ definition = lambda do |name, **capabilities|
30
+ Definition.new(
31
+ name: name,
32
+ record_hash: true,
33
+ transform_container: true,
34
+ app_write: false,
35
+ integration_write: false,
36
+ propagate: false,
37
+ emit_by_default: true,
38
+ delete_paths: false,
39
+ stack: false,
40
+ **capabilities
41
+ )
46
42
  end
47
43
 
48
44
  DEFINITIONS = {
49
- execution: define(:execution, integration_write: true, propagate: true),
50
- context: define(
45
+ execution: definition.call(:execution, integration_write: true, propagate: true),
46
+ context: definition.call(
51
47
  :context,
52
48
  app_write: true,
53
49
  integration_write: true,
54
50
  propagate: true,
55
51
  stack: true
56
52
  ),
57
- carry: define(
53
+ carry: definition.call(
58
54
  :carry,
59
55
  app_write: true,
60
56
  integration_write: true,
@@ -63,18 +59,18 @@ module Julewire
63
59
  delete_paths: true,
64
60
  stack: true
65
61
  ),
66
- neutral: define(:neutral, integration_write: true, emit_by_default: false, stack: true),
67
- attributes: define(
62
+ neutral: definition.call(:neutral, integration_write: true, emit_by_default: false, stack: true),
63
+ attributes: definition.call(
68
64
  :attributes,
69
65
  app_write: true,
70
66
  integration_write: true,
71
67
  stack: true
72
68
  ),
73
- labels: define(:labels),
74
- payload: define(:payload),
75
- metrics: define(:metrics),
76
- error: define(:error, record_hash: false),
77
- summary: define(
69
+ labels: definition.call(:labels),
70
+ payload: definition.call(:payload),
71
+ metrics: definition.call(:metrics),
72
+ error: definition.call(:error, record_hash: false),
73
+ summary: definition.call(
78
74
  :summary,
79
75
  record_hash: false,
80
76
  transform_container: false,
@@ -5,16 +5,22 @@ module Julewire
5
5
  module Fields
6
6
  # @api integration_spi
7
7
  module FieldSet
8
+ Serializer = Serialization::Serializer
9
+ TRUNCATION_METADATA_KEY = Serializer::TRUNCATION_METADATA_KEY.to_sym
10
+ RESERVED_TRUNCATION_METADATA_ERROR =
11
+ "#{Serializer::TRUNCATION_METADATA_KEY} is reserved for Julewire truncation metadata".freeze
12
+ INVALID_MODES = %i[ignore raise wrap].freeze
8
13
  VALUE_KEY = :value
14
+ private_constant :Serializer, :TRUNCATION_METADATA_KEY, :RESERVED_TRUNCATION_METADATA_ERROR, :INVALID_MODES
9
15
 
10
16
  class << self
11
17
  # Public ingress accepts String or Symbol keys. Core stores Symbol keys
12
18
  # after normalization so extension contracts stay simple.
13
- def coerce(fields = nil, keyword_fields = {}, invalid: :ignore)
19
+ def coerce(fields = nil, keyword_fields = nil, invalid: :ignore)
20
+ validate_invalid_mode!(invalid)
14
21
  coerced = {}
15
22
  coerce_fields!(coerced, fields, invalid: invalid) unless fields.nil?
16
- merge!(coerced, keyword_fields) unless keyword_fields.empty?
17
- coerced
23
+ merge!(coerced, keyword_fields)
18
24
  end
19
25
 
20
26
  def merge(left, right)
@@ -25,7 +31,7 @@ module Julewire
25
31
  return target unless fields.is_a?(Hash)
26
32
 
27
33
  fields.each do |key, value|
28
- target[Fields::Internal.normalize_key(key)] = copy_field_value(value)
34
+ target[normalized_field_key(key)] = copy_field_value(value)
29
35
  end
30
36
 
31
37
  target
@@ -48,24 +54,18 @@ module Julewire
48
54
  end
49
55
 
50
56
  def frozen_copy(value)
51
- Fields::Internal.frozen_copy(value)
57
+ Internal.frozen_copy(value)
52
58
  end
53
59
 
54
60
  def value_for(hash, key, default: nil)
55
61
  return default unless hash.is_a?(Hash)
56
62
 
57
- normalized = key.is_a?(String) ? Fields::Internal.normalize_key(key) : key
58
- return hash[normalized] if hash.key?(normalized)
59
-
60
- default
63
+ hash.fetch(Internal.normalize_key(key), default)
61
64
  end
62
65
 
63
66
  private
64
67
 
65
68
  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
69
  Serialization::ValueCopy.call(
70
70
  value,
71
71
  preserve_truncation_metadata: preserve_truncation_metadata
@@ -73,14 +73,11 @@ module Julewire
73
73
  end
74
74
 
75
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
76
  Serialization::ValueCopy.call(
80
77
  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,
78
+ max_array_items: Serializer::DEFAULT_MAX_ARRAY_ITEMS,
79
+ max_hash_keys: Serializer::DEFAULT_MAX_HASH_KEYS,
80
+ max_string_bytes: Serializer::DEFAULT_MAX_STRING_BYTES,
84
81
  preserve_truncation_metadata: preserve_truncation_metadata,
85
82
  symbolize_keys: true
86
83
  )
@@ -90,12 +87,25 @@ module Julewire
90
87
  if fields.is_a?(Hash)
91
88
  merge!(target, fields)
92
89
  elsif invalid == :wrap
93
- target[VALUE_KEY] = deep_dup(fields)
90
+ target[VALUE_KEY] = copy_field_value(fields)
94
91
  elsif invalid == :raise
95
92
  raise ArgumentError, "fields must be a Hash"
96
93
  end
97
94
  end
98
95
 
96
+ def validate_invalid_mode!(invalid)
97
+ return if INVALID_MODES.include?(invalid)
98
+
99
+ raise ArgumentError, "invalid field coercion mode: #{invalid.inspect}"
100
+ end
101
+
102
+ def normalized_field_key(key)
103
+ normalized_key = Internal.normalize_key(key)
104
+ raise ArgumentError, RESERVED_TRUNCATION_METADATA_ERROR if normalized_key == TRUNCATION_METADATA_KEY
105
+
106
+ normalized_key
107
+ end
108
+
99
109
  def copy_field_value(value) = deep_symbolize_keys(value)
100
110
  end
101
111
  end
@@ -4,8 +4,8 @@ module Julewire
4
4
  module Core
5
5
  module Fields
6
6
  # @api internal
7
- # Immutable layers keep snapshots stable while each stack tracks only its
8
- # current head and versioned read caches.
7
+ # Immutable layers keep snapshots stable while each stack tracks only the
8
+ # current head and snapshot cache.
9
9
  class FieldStack
10
10
  EMPTY_HASH = {}.freeze
11
11
  private_constant :EMPTY_HASH
@@ -13,26 +13,37 @@ 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, owned: false)
16
+ class << self
17
+ def fields(parent, fields, clear_parent_deletes: true)
18
+ clear_parent_fields = fields if clear_parent_deletes
19
+ new(
20
+ parent,
21
+ fields,
22
+ delete_paths: nil,
23
+ clear_parent_fields:
24
+ )
25
+ end
26
+
27
+ def delete_paths(parent, paths)
28
+ new(parent, nil, delete_paths: paths, clear_parent_fields: nil)
29
+ end
30
+
31
+ private :new
32
+ end
33
+
34
+ def initialize(parent, fields, delete_paths:, clear_parent_fields:)
17
35
  @parent = parent
18
36
  @fields = fields
19
37
  @delete_paths = delete_paths
20
- @clear_parent_deletes = clear_parent_deletes
21
- @owned = owned
22
- @active_delete_paths_computed = false
23
- @active_delete_paths = nil
24
- @snapshot = nil
25
- @value_cache = nil
38
+ @clear_parent_fields = clear_parent_fields
26
39
  end
27
40
 
28
- def owned? = @owned
29
-
30
41
  def snapshot
31
42
  @snapshot ||= build_snapshot
32
43
  end
33
44
 
34
45
  def value_for(key)
35
- return @value_cache[key] if @value_cache&.key?(key)
46
+ return @value_cache.fetch(key) if @value_cache&.key?(key)
36
47
 
37
48
  value = if delete_paths_for_key?(key)
38
49
  FieldSet.value_for(snapshot, key, default: MISSING)
@@ -44,82 +55,42 @@ module Julewire
44
55
  end
45
56
 
46
57
  def active_delete_paths
47
- return @active_delete_paths if @active_delete_paths_computed
58
+ return @active_delete_paths if instance_variable_defined?(:@active_delete_paths)
48
59
 
49
60
  @active_delete_paths = build_active_delete_paths
50
- @active_delete_paths_computed = true
51
- @active_delete_paths
52
61
  end
53
62
 
54
63
  def snapshot_cached?
55
64
  !@snapshot.nil?
56
65
  end
57
66
 
58
- def delete_paths_for_snapshot
59
- @clear_parent_deletes ? @delete_paths : active_delete_paths
60
- end
61
-
62
67
  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
+ Internal.merge_owned!(snapshot, FieldSet.deep_dup_owned(@fields))
68
69
  end
69
70
 
70
71
  private
71
72
 
72
73
  def build_snapshot
73
- return build_direct_snapshot unless @parent
74
- return build_parent_snapshot if @parent.snapshot_cached?
75
-
76
- snapshot = source_snapshot_base
77
- source_chain.reverse_each do |source|
78
- source.merge_into(snapshot)
79
- paths = source.delete_paths_for_snapshot
80
- Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
81
- end
82
- Fields::Internal.frozen_owned_copy(snapshot)
83
- end
84
-
85
- def build_direct_snapshot
86
- snapshot = merge_into({})
87
- paths = delete_paths_for_snapshot
88
- Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
89
- Fields::Internal.frozen_owned_copy(snapshot)
90
- end
91
-
92
- def build_parent_snapshot
93
- snapshot = FieldSet.deep_dup_owned(@parent.snapshot)
94
- merge_into(snapshot)
95
- paths = delete_paths_for_snapshot
96
- Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
97
- Fields::Internal.frozen_owned_copy(snapshot)
98
- end
99
-
100
- def source_snapshot_base
101
- source = source_chain_base
102
- source ? FieldSet.deep_dup_owned(source.snapshot) : {}
74
+ sources, base = source_layers_and_base
75
+ snapshot = base ? FieldSet.deep_dup_owned(base.snapshot) : {}
76
+ sources.reverse_each { it.merge_into(snapshot) }
77
+ paths = active_delete_paths
78
+ Internal.apply_delete_paths!(snapshot, paths) if paths
79
+ Internal.frozen_owned_copy(snapshot)
103
80
  end
104
81
 
105
82
  def frozen_field_value(value)
106
- @owned ? Fields::Internal.frozen_owned_copy(value) : Fields::Internal.frozen_copy(value)
83
+ Internal.frozen_owned_copy(value)
107
84
  end
108
85
 
109
- def source_chain
110
- sources = []
111
- source = self
112
- until source.nil? || source.snapshot_cached?
113
- sources << source
114
- source = source.parent
115
- end
116
- sources
117
- end
86
+ def source_layers_and_base(source = self, seen_sources = [], sources = [])
87
+ return [sources, source] if source.nil? || source.snapshot_cached?
88
+
89
+ raise Error, "field stack layer cycle" if seen_sources.include?(source)
118
90
 
119
- def source_chain_base
120
- source = self
121
- source = source.parent until source.nil? || source.snapshot_cached?
122
- source
91
+ seen_sources << source
92
+ sources << source
93
+ source_layers_and_base(source.parent, seen_sources, sources)
123
94
  end
124
95
 
125
96
  def parent_value_for(key)
@@ -134,16 +105,14 @@ module Julewire
134
105
 
135
106
  def build_active_delete_paths
136
107
  paths = @parent&.active_delete_paths
137
- paths = clear_active_delete_paths(paths) if paths && @clear_parent_deletes && !@fields.empty?
108
+ paths = clear_active_delete_paths(paths) if paths
138
109
  paths = append_delete_paths(paths) if @delete_paths
139
- return unless paths
140
-
141
- paths.empty? ? nil : paths
110
+ paths
142
111
  end
143
112
 
144
113
  def clear_active_delete_paths(paths)
145
114
  paths = paths.dup
146
- Fields::Internal.clear_delete_paths!(paths, @fields)
115
+ Internal.clear_delete_paths!(paths, @clear_parent_fields)
147
116
  paths
148
117
  end
149
118
 
@@ -153,51 +122,35 @@ module Julewire
153
122
  end
154
123
  private_constant :Layer
155
124
 
156
- def initialize(fields = {}, delete_paths: false, source: nil)
125
+ def initialize(fields = nil, delete_paths: false, source: nil)
157
126
  @source = source
158
127
  @delete_paths_enabled = delete_paths
159
- @version = 0
160
- @snapshot_version = nil
161
- @snapshot = nil
162
- @value_cache = nil
163
- add(fields) if fields.is_a?(Hash) && !fields.empty?
128
+ add(fields)
164
129
  end
165
130
 
166
131
  def snapshot
167
- return @snapshot if @snapshot_version == @version
132
+ return @snapshot if @snapshot
168
133
 
169
134
  @snapshot = @source ? @source.snapshot : EMPTY_HASH
170
- @snapshot_version = @version
171
- @snapshot
172
135
  end
173
136
 
174
- def fork
137
+ def branch
175
138
  self.class.new(delete_paths: @delete_paths_enabled, source: @source)
176
139
  end
177
140
 
178
141
  def value_for(key, default:)
179
- cache = @value_cache
180
- return cache[key] if cache&.key?(key)
181
-
182
- if key.is_a?(String)
183
- key = Fields::Internal.normalize_key(key)
184
- cache = @value_cache
185
- return cache[key] if cache&.key?(key)
186
- end
187
-
142
+ key = Internal.normalize_key(key)
188
143
  value = source_value_for(key)
189
144
  return default if value.equal?(MISSING)
190
145
 
191
- (@value_cache ||= {})[key] = value
146
+ value
192
147
  end
193
148
 
194
149
  def add(fields = nil, owned: false, **keyword_fields)
195
150
  fields = field_input(fields, keyword_fields, owned: owned)
196
- return unless fields.is_a?(Hash)
197
151
  return if fields.empty?
198
152
 
199
- fields = normalize_owned_keys(fields) if owned
200
- @source = Layer.new(@source, fields, clear_parent_deletes: true, owned: owned)
153
+ @source = Layer.fields(@source, fields)
201
154
  invalidate_snapshot!
202
155
  end
203
156
 
@@ -205,17 +158,15 @@ module Julewire
205
158
  return if path.empty?
206
159
  return unless @delete_paths_enabled
207
160
 
208
- @source = Layer.new(@source, {}, delete_paths: [path], clear_parent_deletes: false)
161
+ @source = Layer.delete_paths(@source, [path])
209
162
  invalidate_snapshot!
210
163
  end
211
164
 
212
165
  def with(fields = nil, owned: false, **keyword_fields, &)
213
166
  fields = field_input(fields, keyword_fields, owned: owned)
214
- return yield unless fields.is_a?(Hash)
215
167
  return yield if fields.empty?
216
168
 
217
- fields = normalize_owned_keys(fields) if owned
218
- with_layer(fields, owned: owned, &)
169
+ with_layer(fields, &)
219
170
  end
220
171
 
221
172
  def without(path, &)
@@ -223,34 +174,25 @@ module Julewire
223
174
 
224
175
  return yield unless @delete_paths_enabled
225
176
 
226
- with_layer({}, delete_paths: [path], &)
177
+ with_delete_layer([path], &)
227
178
  end
228
179
 
229
180
  private
230
181
 
231
182
  def field_input(fields, keyword_fields, owned:)
232
183
  if owned
233
- return fields if keyword_fields.empty?
234
- return keyword_fields if fields.nil?
235
- return fields.merge(keyword_fields) if fields.is_a?(Hash)
184
+ fields = keyword_fields if fields.nil?
185
+ Serialization::DeepFreeze.validate_symbol_hash(fields)
236
186
 
237
- return keyword_fields
187
+ return fields.merge(keyword_fields)
238
188
  end
239
189
 
240
- return FieldSet.deep_symbolize_keys(fields) if keyword_fields.empty?
241
-
242
190
  FieldSet.coerce(fields, keyword_fields)
243
191
  end
244
192
 
245
- def with_layer(fields, delete_paths: nil, owned: false)
193
+ def with_layer(fields)
246
194
  previous_source = @source
247
- @source = Layer.new(
248
- previous_source,
249
- fields,
250
- delete_paths: delete_paths,
251
- clear_parent_deletes: false,
252
- owned: owned
253
- )
195
+ @source = Layer.fields(previous_source, fields, clear_parent_deletes: false)
254
196
  invalidate_snapshot!
255
197
  begin
256
198
  yield
@@ -260,34 +202,26 @@ module Julewire
260
202
  end
261
203
  end
262
204
 
263
- def normalize_owned_keys(fields)
264
- return fields unless fields.any? { |key, _value| key.is_a?(String) }
265
-
266
- fields.to_h { |key, value| [Fields::Internal.normalize_key(key), value] }
205
+ def with_delete_layer(delete_paths)
206
+ previous_source = @source
207
+ @source = Layer.delete_paths(previous_source, delete_paths)
208
+ invalidate_snapshot!
209
+ begin
210
+ yield
211
+ ensure
212
+ @source = previous_source
213
+ invalidate_snapshot!
214
+ end
267
215
  end
268
216
 
269
217
  def source_value_for(key)
270
218
  return MISSING unless @source
271
219
 
272
- unless @source.parent
273
- # Single-layer hits avoid Layer's delete-path/cache bookkeeping.
274
- field_value = FieldSet.value_for(@source.fields, key, default: MISSING)
275
- return frozen_source_value(field_value, @source.owned?) unless field_value.equal?(MISSING)
276
- end
277
-
278
- value = @source.value_for(key)
279
- value.equal?(MISSING) ? MISSING : value
280
- end
281
-
282
- def frozen_source_value(value, owned)
283
- owned ? Fields::Internal.frozen_owned_copy(value) : Fields::Internal.frozen_copy(value)
220
+ @source.value_for(key)
284
221
  end
285
222
 
286
223
  def invalidate_snapshot!
287
- @version += 1
288
224
  @snapshot = nil
289
- @snapshot_version = nil
290
- @value_cache = nil
291
225
  end
292
226
  end
293
227
  end
@@ -11,12 +11,10 @@ module Julewire
11
11
  return target if normalized_path.empty?
12
12
 
13
13
  deep_delete_path!(target, normalized_path)
14
- target
15
14
  end
16
15
 
17
16
  def apply_delete_paths!(target, paths)
18
17
  paths.each { delete_path!(target, it) }
19
- target
20
18
  end
21
19
 
22
20
  def clear_delete_paths!(paths, fields)
@@ -27,7 +25,7 @@ module Julewire
27
25
  end
28
26
 
29
27
  def normalize_path(path)
30
- Array(path).flatten.filter_map { Internal.normalize_key(it) }
28
+ Array(path).flatten.map { Internal.normalize_key(it) }
31
29
  end
32
30
 
33
31
  private
@@ -37,7 +35,7 @@ module Julewire
37
35
 
38
36
  fields.flat_map do |key, value|
39
37
  path = prefix + [Internal.normalize_key(key)]
40
- nested = value.is_a?(Hash) ? field_paths(value, path) : []
38
+ nested = field_paths(value, path)
41
39
  nested.empty? ? [path] : nested
42
40
  end
43
41
  end
@@ -53,12 +51,11 @@ module Julewire
53
51
  key = path.first
54
52
  if path.one?
55
53
  Internal.delete_key!(target, key)
56
- return
54
+ else
55
+ child = FieldSet.value_for(target, key)
56
+ deep_delete_path!(child, path.drop(1))
57
+ Internal.delete_key!(target, key) if child.is_a?(Hash) && child.empty?
57
58
  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
59
  end
63
60
  end
64
61
  end
@@ -4,13 +4,16 @@ module Julewire
4
4
  module Core
5
5
  module Fields
6
6
  module Internal
7
- EMPTY_ARRAY = [].freeze
8
- EMPTY_HASH = {}.freeze
9
- private_constant :EMPTY_ARRAY, :EMPTY_HASH
7
+ FIELD_KEY_ERROR = "field keys must be String or Symbol"
8
+ RECORD_STRING_KEY_ERROR = "record must not use string keys"
9
+ RECORD_SYMBOL_KEY_ERROR = "record keys must be Symbols"
10
10
 
11
11
  class << self
12
12
  def normalize_key(key)
13
- key.is_a?(String) ? key.to_sym : key
13
+ return key.to_sym if key.is_a?(String)
14
+ return key if key.instance_of?(Symbol)
15
+
16
+ raise TypeError, FIELD_KEY_ERROR
14
17
  end
15
18
 
16
19
  def delete_key!(target, key)
@@ -25,26 +28,12 @@ module Julewire
25
28
  frozen_copy_with(value, preserve_truncation_metadata: true)
26
29
  end
27
30
 
28
- def frozen_deep_symbolize_keys(value)
29
- frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata: false)
30
- end
31
-
32
- def frozen_deep_symbolize_owned_keys(value)
33
- frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata: true)
34
- end
35
-
36
- def delete_path!(target, path) = Deletion.delete_path!(target, path)
37
-
38
31
  def apply_delete_paths!(target, paths) = Deletion.apply_delete_paths!(target, paths)
39
32
 
40
33
  def clear_delete_paths!(paths, fields) = Deletion.clear_delete_paths!(paths, fields)
41
34
 
42
35
  def normalize_path(path) = Deletion.normalize_path(path)
43
36
 
44
- def deep_merge(left, right)
45
- deep_merge!(FieldSet.deep_symbolize_keys(left), right)
46
- end
47
-
48
37
  def deep_merge!(target, fields)
49
38
  merge_values!(target, fields) do |value, existing|
50
39
  if existing.is_a?(Hash) && value.is_a?(Hash)
@@ -56,7 +45,7 @@ module Julewire
56
45
  end
57
46
 
58
47
  def deep_merge_owned!(target, fields)
59
- merge_values!(target, fields) do |value, existing|
48
+ merge_owned_values!(target, fields) do |value, existing|
60
49
  if existing.is_a?(Hash) && value.is_a?(Hash)
61
50
  deep_merge_owned!(existing, value)
62
51
  else
@@ -66,15 +55,12 @@ module Julewire
66
55
  end
67
56
 
68
57
  def merge_owned!(target, fields)
69
- merge_values!(target, fields) { |value, _existing| value }
58
+ merge_owned_values!(target, fields) { |value, _existing| value }
70
59
  end
71
60
 
72
61
  private
73
62
 
74
63
  def frozen_copy_with(value, preserve_truncation_metadata:)
75
- return EMPTY_HASH if value.is_a?(Hash) && value.empty?
76
- return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
77
-
78
64
  Serialization::ValueCopy.call(
79
65
  value,
80
66
  freeze_values: true,
@@ -82,21 +68,6 @@ module Julewire
82
68
  )
83
69
  end
84
70
 
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?
88
-
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
99
-
100
71
  def merge_values!(target, fields)
101
72
  return target unless fields.is_a?(Hash)
102
73
 
@@ -108,6 +79,16 @@ module Julewire
108
79
 
109
80
  target
110
81
  end
82
+
83
+ def merge_owned_values!(target, fields)
84
+ return target unless fields.is_a?(Hash)
85
+
86
+ fields.each do |key, value|
87
+ target[key] = yield value, target[key]
88
+ end
89
+
90
+ target
91
+ end
111
92
  end
112
93
  end
113
94
  end