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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ class Subscription
8
+ attr_reader :subscriber
9
+
10
+ def initialize(subscriber, unsubscribe: nil)
11
+ @subscriber = subscriber
12
+ @unsubscribe = unsubscribe
13
+ end
14
+
15
+ def update(configuration)
16
+ @subscriber.configuration = configuration
17
+ @subscriber
18
+ end
19
+
20
+ def reset
21
+ @unsubscribe&.call
22
+ nil
23
+ rescue StandardError
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Julewire
6
+ module Core
7
+ module Integration
8
+ # @api integration_spi
9
+ module Values
10
+ EMPTY_HASH = {}.freeze
11
+
12
+ module Common
13
+ def empty_hash = EMPTY_HASH
14
+
15
+ def blank_value?(value)
16
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
17
+ rescue StandardError
18
+ false
19
+ end
20
+ end
21
+
22
+ private_constant :EMPTY_HASH, :Common
23
+
24
+ # @api integration_spi
25
+ module Read
26
+ extend Common
27
+
28
+ class << self
29
+ def blank?(value)
30
+ blank_value?(value)
31
+ end
32
+
33
+ def hash_value(hash, key, default: nil)
34
+ return default unless hash.is_a?(Hash)
35
+
36
+ direct_hash_value(hash, key, default)
37
+ end
38
+
39
+ def value(object, key, default: nil)
40
+ return hash_value(object, key, default: default) if object.is_a?(Hash)
41
+
42
+ return default unless object.respond_to?(key)
43
+
44
+ object.public_send(key)
45
+ rescue StandardError
46
+ default
47
+ end
48
+
49
+ def nested_value(object, *keys, default: nil)
50
+ current = object
51
+ keys.each do |key|
52
+ return default if current.nil?
53
+
54
+ current = value(current, key)
55
+ end
56
+ current.nil? ? default : current
57
+ end
58
+
59
+ def path_value(object, path, default: nil)
60
+ current = object
61
+ Array(path).each do |key|
62
+ return default if current.nil?
63
+
64
+ current = indexed_value(current, key)
65
+ return default if current.equal?(MISSING)
66
+ end
67
+ current
68
+ end
69
+
70
+ def first_value(source, keys:)
71
+ if source.is_a?(Hash)
72
+ found = direct_hash_first_value(source, keys)
73
+ return found unless found.equal?(MISSING)
74
+ end
75
+
76
+ keys.each do |key|
77
+ found = indexed_value(source, key)
78
+ return found unless found.equal?(MISSING) || blank_value?(found)
79
+ end
80
+ nil
81
+ end
82
+
83
+ private
84
+
85
+ def direct_hash_first_value(source, keys)
86
+ keys.each do |key|
87
+ next unless source.key?(key)
88
+
89
+ found = source[key]
90
+ return found unless blank_value?(found)
91
+ end
92
+ MISSING
93
+ end
94
+
95
+ def indexed_value(source, key)
96
+ return hash_value(source, key, default: MISSING) if source.is_a?(Hash)
97
+ return MISSING unless source.respond_to?(:[])
98
+
99
+ source[key]
100
+ rescue StandardError
101
+ MISSING
102
+ end
103
+
104
+ def direct_hash_value(hash, key, default)
105
+ return hash[key] if hash.key?(key)
106
+
107
+ case key
108
+ when Symbol then symbol_key_value(hash, key, default)
109
+ when String then string_key_value(hash, key, default)
110
+ else default
111
+ end
112
+ end
113
+
114
+ def symbol_key_value(hash, key, default)
115
+ string_key = key.name
116
+ return hash[string_key] if hash.key?(string_key)
117
+
118
+ default
119
+ end
120
+
121
+ def string_key_value(hash, key, default)
122
+ symbol_key = key.to_sym
123
+ return hash[symbol_key] if hash.key?(symbol_key)
124
+
125
+ default
126
+ end
127
+ end
128
+ end
129
+
130
+ # @api integration_spi
131
+ module Shape
132
+ extend Common
133
+
134
+ class << self
135
+ def timestamp(value)
136
+ return unless value
137
+ return value.utc.iso8601(9) if value.respond_to?(:utc) && value.respond_to?(:iso8601)
138
+ return value unless value.respond_to?(:divmod)
139
+
140
+ seconds, nanoseconds = value.divmod(1_000_000_000)
141
+ Time.at(seconds, nanoseconds, :nanosecond).utc.iso8601(9)
142
+ rescue StandardError
143
+ nil
144
+ end
145
+
146
+ def payload_hash(value)
147
+ case value
148
+ when nil
149
+ empty_hash
150
+ when Hash
151
+ return empty_hash if value.empty?
152
+
153
+ Fields::FieldSet.deep_symbolize_keys(value)
154
+ else
155
+ { Fields::FieldSet::VALUE_KEY => value }
156
+ end
157
+ end
158
+
159
+ def hash_or_empty(value)
160
+ return empty_hash unless value.is_a?(Hash)
161
+ return empty_hash if value.empty?
162
+
163
+ Fields::FieldSet.deep_symbolize_keys(value)
164
+ end
165
+
166
+ def append_field(fields, key, value, compact_empty: false)
167
+ return if value.nil?
168
+ return if compact_empty && (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
169
+
170
+ fields[key] = value
171
+ nil
172
+ end
173
+
174
+ def append_compact_field(fields, key, value)
175
+ append_field(fields, key, value, compact_empty: true)
176
+ end
177
+
178
+ def source_location_attributes(location)
179
+ return {} unless location.is_a?(Hash)
180
+
181
+ Fields::AttributeKeys.fields(
182
+ Fields::AttributeKeys::CODE_FILE_PATH => Read.first_value(location, keys: %i[filepath path file]),
183
+ Fields::AttributeKeys::CODE_LINE_NUMBER => Read.first_value(location, keys: %i[lineno line]),
184
+ Fields::AttributeKeys::CODE_FUNCTION_NAME => Read.first_value(location, keys: %i[label function])
185
+ )
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class LifecycleError < Error; end
6
+ end
7
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/atomic/atomic_reference"
4
+
5
+ module Julewire
6
+ module Core
7
+ # @api internal
8
+ # Process/ractor-local storage for facade lookups. The ractor bridge can set
9
+ # the current runtime from inside a child ractor, but the getter side still
10
+ # runs inside core through Julewire.* facade calls.
11
+ module LocalStorage
12
+ RUNTIME_KEY = :__julewire_core_runtime__
13
+ CONTEXT_STORE_THREAD_KEY = :__julewire_core_context_store__
14
+ CONTEXT_STORE_FIBER_IVAR = :@__julewire_core_context_store__
15
+ private_constant :RUNTIME_KEY, :CONTEXT_STORE_THREAD_KEY, :CONTEXT_STORE_FIBER_IVAR
16
+
17
+ @runtime_ref = Concurrent::AtomicReference.new
18
+ @runtime_mutex = Mutex.new
19
+
20
+ class << self
21
+ def runtime
22
+ return ractor_runtime if ractor_local_storage?
23
+
24
+ runtime_ref.get || runtime_mutex.synchronize do
25
+ runtime_ref.get || Runtime.new.tap { runtime_ref.set(it) }
26
+ end
27
+ end
28
+
29
+ def runtime=(runtime)
30
+ if ractor_local_storage?
31
+ ::Ractor[RUNTIME_KEY] = runtime
32
+ else
33
+ runtime_ref.set(runtime)
34
+ end
35
+ end
36
+
37
+ def context_store
38
+ context_store_value || store_context(ContextStore.new)
39
+ end
40
+
41
+ def reset_context_store!
42
+ store_context(nil)
43
+ end
44
+
45
+ def after_fork!
46
+ runtime = runtime_ref.get
47
+ @runtime_mutex = Mutex.new
48
+ @runtime_ref = Concurrent::AtomicReference.new(runtime)
49
+ nil
50
+ end
51
+
52
+ # Private testing seam for storage-selection behavior.
53
+ def main_ractor?
54
+ ::Ractor.main?
55
+ end
56
+ private :main_ractor?
57
+
58
+ private
59
+
60
+ attr_reader :runtime_mutex, :runtime_ref
61
+
62
+ def ractor_runtime
63
+ ::Ractor.store_if_absent(RUNTIME_KEY) { Runtime.new }
64
+ end
65
+
66
+ def context_store_value
67
+ if ractor_local_storage?
68
+ # Child-ractor bridge work is thread-scoped; main-ractor app work is fiber-scoped.
69
+ Thread.current[CONTEXT_STORE_THREAD_KEY]
70
+ else
71
+ # Do not use Fiber#storage here: child fibers inherit it by default,
72
+ # while Julewire context only propagates through Julewire.fiber.
73
+ Fiber.current.instance_variable_get(CONTEXT_STORE_FIBER_IVAR)
74
+ end
75
+ end
76
+
77
+ def store_context(value)
78
+ if ractor_local_storage?
79
+ Thread.current[CONTEXT_STORE_THREAD_KEY] = value
80
+ else
81
+ Fiber.current.instance_variable_set(CONTEXT_STORE_FIBER_IVAR, value)
82
+ end
83
+ end
84
+
85
+ def ractor_local_storage?
86
+ !main_ractor?
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Processing
6
+ class LevelThreshold
7
+ DEFAULT_EVENT = "log"
8
+ DEFAULT_SEVERITY = :info
9
+
10
+ attr_reader :level
11
+
12
+ def initialize(level:, invalid_severity_reporter: Diagnostics::InvalidSeverityReporter)
13
+ @level = Records::Severity.normalize(level)
14
+ @level_rank = Records::Severity.rank(@level)
15
+ @invalid_severity_reporter = invalid_severity_reporter
16
+ end
17
+
18
+ def allow?(severity)
19
+ Records::Severity.rank(severity) >= @level_rank
20
+ end
21
+
22
+ def raw_input_allowed?(input)
23
+ return allow?(DEFAULT_SEVERITY) unless Records::RawInput.hash_input?(input)
24
+
25
+ severity, invalid, invalid_raw_value = raw_input_severity(input)
26
+ allowed = allow?(severity)
27
+ # Surviving inputs warn later at Records::Draft normalization.
28
+ record_invalid_raw_severity(input, invalid_raw_value) if invalid && !allowed
29
+ allowed
30
+ end
31
+
32
+ private
33
+
34
+ def raw_input_severity(input)
35
+ return [DEFAULT_SEVERITY, false, nil] unless Records::RawInput.explicit_severity?(input)
36
+
37
+ raw_value = Records::RawInput.value(input, :severity)
38
+ [Records::Severity.normalize(raw_value), false, nil]
39
+ rescue ArgumentError
40
+ [DEFAULT_SEVERITY, true, raw_value]
41
+ end
42
+
43
+ def record_invalid_raw_severity(input, raw_value)
44
+ @invalid_severity_reporter.call(
45
+ raw_value,
46
+ source: Records::RawInput.value(input, :source),
47
+ event: Records::RawInput.value(input, :event) || DEFAULT_EVENT
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Processing
6
+ # @api extension
7
+ class Match
8
+ Rule = Data.define(:conditions, :handler)
9
+ private_constant :Rule
10
+
11
+ def initialize(&)
12
+ @rules = []
13
+ instance_eval(&) if block_given?
14
+ end
15
+
16
+ def on(conditions = nil, **keyword_conditions, &handler)
17
+ raise ArgumentError, "match handler is required" unless handler
18
+
19
+ conditions = normalize_conditions(conditions, keyword_conditions)
20
+ @rules << Rule.new(conditions.freeze, handler)
21
+ self
22
+ end
23
+
24
+ def call(draft)
25
+ @rules.each do |rule|
26
+ next unless matches_conditions?(draft, rule.conditions)
27
+
28
+ result = rule.handler.call(draft)
29
+ return result if result == :drop || result.is_a?(Records::Draft)
30
+ end
31
+ nil
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_conditions(conditions, keyword_conditions)
37
+ fields = case conditions
38
+ when nil then {}
39
+ when Hash then conditions.dup
40
+ else raise ArgumentError, "match conditions must be a Hash"
41
+ end
42
+ fields.merge!(keyword_conditions)
43
+ raise ArgumentError, "match conditions are required" if fields.empty?
44
+
45
+ fields
46
+ end
47
+
48
+ def matches_conditions?(draft, conditions)
49
+ conditions.all? { |key, pattern| matches_value?(pattern, draft[key]) }
50
+ end
51
+
52
+ def matches_value?(pattern, value)
53
+ case pattern
54
+ when Hash then matches_hash?(pattern, value)
55
+ when Proc then pattern.call(value)
56
+ when Regexp then value.is_a?(String) && pattern.match?(value)
57
+ when Range then pattern.cover?(value)
58
+ when Module then value.is_a?(pattern)
59
+ else pattern == value
60
+ end
61
+ end
62
+
63
+ def matches_hash?(pattern, value)
64
+ return false unless value.is_a?(Hash)
65
+
66
+ pattern.all? do |key, nested_pattern|
67
+ nested_value = Fields::FieldSet.value_for(value, key, default: Core::UNSET)
68
+ !nested_value.equal?(Core::UNSET) && matches_value?(nested_pattern, nested_value)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end