console 1.35.1 → 1.36.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e161d51958a96742a44541c862e32c9354c04d3cdae04cfe57ef0a63b2ecfd36
4
- data.tar.gz: 48afe3e18acbc12cd72b96c22a139fd9de69e852c0ba8e98a4367811ca1e916e
3
+ metadata.gz: 749f01df7fec2a4868a2b4958b23ee328ddb2911b0fc2a635bdf298d901cae60
4
+ data.tar.gz: 32daa46920a77f0fa713685d0176dc24ed034fe39fe99cde3d1d789ecfb1c48b
5
5
  SHA512:
6
- metadata.gz: 2caf9a3c2520460b7a54489bf4ed68a9f0f37223a45917500884b8f5a1c531ebb602a1dda943994925c716d9af785c11c07f2263704572db3ed343f9858c75a6
7
- data.tar.gz: aaf33117df4b08e942b8975128bb11a11b653580c85dfe93218d5a4af0a16db69385d4907a37d461a3d3e89c6ef6578ecef4aaca3ec07e3d1d63c56c66111283
6
+ metadata.gz: 51796e3366c0778f6e811428ff9e8aeffb0a34fe99ce0528488fd9931c392596c1a654f34e49781f68afd1377898ef16ce6aeaaa3a35c82c201bf4579f423073
7
+ data.tar.gz: 82af50570a7e55a26562cda15f7ccf5622bcc956ef6b6a065f4a1e38ecf663911f84ccf6a47de1443bc85f1b1e79effeec1fcbdccc21480c0b18b53872efdc13
checksums.yaml.gz.sig CHANGED
Binary file
@@ -10,31 +10,166 @@ module Console
10
10
  module Format
11
11
  # A safe format for converting objects to strings.
12
12
  #
13
- # Handles issues like circular references and encoding errors.
13
+ # Handles issues like circular references, encoding errors, excessive nesting depth, and excessive output size.
14
14
  class Safe
15
+ # The JSON fragment used as the truncation marker when dropped fields cannot be named.
16
+ TRUNCATED = "\"truncated\":true"
17
+
15
18
  # Create a new safe format.
16
19
  #
17
20
  # @parameter format [JSON] The format to use for serialization.
18
- # @parameter limit [Integer] The maximum depth to recurse into objects.
21
+ # @parameter depth_limit [Integer] The maximum depth to recurse into objects (the JSON `max_nesting`).
22
+ # @parameter size_limit [Integer | Nil] The maximum byte size of the serialized output, or `nil` to disable size limiting. Limits below {TRUNCATED} (the minimal marker) cannot be honoured.
19
23
  # @parameter encoding [Encoding] The encoding to use for strings.
20
- def initialize(format: ::JSON, limit: 12, encoding: ::Encoding::UTF_8)
24
+ # @parameter limit [Integer | Nil] Deprecated alias for `depth_limit`.
25
+ def initialize(format: ::JSON, depth_limit: 12, size_limit: 16 * 1024, encoding: ::Encoding::UTF_8, limit: nil)
26
+ if limit
27
+ warn "Console::Format::Safe `limit:` is deprecated, use `depth_limit:` instead.", uplevel: 1, category: :deprecated
28
+ depth_limit = limit
29
+ end
30
+
21
31
  @format = format
22
- @limit = limit
32
+ @depth_limit = depth_limit
33
+ @size_limit = size_limit
23
34
  @encoding = encoding
24
35
  end
25
36
 
37
+ # @attribute [Integer] The maximum depth to recurse into objects.
38
+ attr :depth_limit
39
+
40
+ # @attribute [Integer | Nil] The maximum byte size of the serialized output.
41
+ attr :size_limit
42
+
26
43
  # Dump the given object to a string.
27
44
  #
45
+ # The common case is a single fast serialization. If that fails (e.g. circular
46
+ # references, excessive nesting, or encoding errors) or its output exceeds
47
+ # {size_limit}, it falls back to {safe_dump}, which rebuilds the record
48
+ # field-by-field within the limit.
49
+ #
28
50
  # @parameter object [Object] The object to dump.
29
51
  # @returns [String] The dumped object.
30
52
  def dump(object)
31
- @format.dump(object, @limit)
32
- rescue SystemStackError, StandardError => error
33
- @format.dump(safe_dump(object, error))
53
+ buffer = @format.dump(object, @depth_limit)
54
+
55
+ if @size_limit and buffer.bytesize > @size_limit
56
+ return safe_dump(object)
57
+ end
58
+
59
+ return buffer
60
+ rescue SystemStackError, StandardError
61
+ return safe_dump(object)
34
62
  end
35
63
 
36
64
  private
37
65
 
66
+ # Produce a safe, size-limited serialization of the given object. This is the
67
+ # fallback path, used both when direct serialization fails (an exception) and
68
+ # when its output exceeds {size_limit}.
69
+ #
70
+ # Each top-level value is serialized independently and defensively, so a single
71
+ # un-serializable or oversized value cannot break or bloat the whole record.
72
+ # Whenever a field is degraded, the reason is recorded in a trailing `"truncated"`
73
+ # object that maps the field name to why it was truncated:
74
+ #
75
+ # - `"key": true` — the value was dropped because it did not fit the size limit.
76
+ # - `"key": {error}` — the value could not be serialized directly; a safe
77
+ # representation was kept in its place and the triggering error is recorded.
78
+ #
79
+ # Fields are kept while they fit, always reserving room for at least a minimal
80
+ # `"truncated":true` marker. The detailed reason map is then emitted only if it
81
+ # fits in the remaining space; otherwise it degrades to `"truncated":true`. This
82
+ # is best-effort — in the worst case the per-field detail is lost — but it keeps
83
+ # the bookkeeping simple and the size guarantee hard.
84
+ #
85
+ # @parameter object [Object] The object to serialize.
86
+ # @returns [String] The safe, size-limited serialized record.
87
+ def safe_dump(object)
88
+ # Serialize hash-like objects field-by-field; anything else falls through to the
89
+ # error handler below, which emits a minimal truncated marker.
90
+ object = object.to_hash
91
+
92
+ # Serialize each field once, capturing the error for any value that could not be
93
+ # serialized directly. Our own "truncated" key is skipped so it is never duplicated.
94
+ errors = {}
95
+ fragments = []
96
+ object.each do |key, value|
97
+ name = key.to_s
98
+ next if name == "truncated"
99
+
100
+ fragment, error = dump_pair(key, value)
101
+ errors[name] = error_info(error) if error
102
+ fragments << [name, fragment]
103
+ end
104
+
105
+ # Assemble the body, keeping each field while it fits — always reserving room for
106
+ # at least a minimal `"truncated":true` marker. Each truncated field's reason is
107
+ # collected: its error (value recovered) or `true` (dropped for size).
108
+ buffer = +"{"
109
+ first = true
110
+ reasons = {}
111
+
112
+ fragments.each do |name, fragment|
113
+ if buffer.bytesize + (first ? 0 : 1) + fragment.bytesize + TRUNCATED.bytesize + 2 <= @size_limit
114
+ buffer << "," unless first
115
+ buffer << fragment
116
+ first = false
117
+
118
+ # The value was kept; if it had to be recovered, note why.
119
+ reasons[name] = errors[name] if errors[name]
120
+ else
121
+ # The value did not fit and was dropped entirely.
122
+ reasons[name] = true
123
+ end
124
+ end
125
+
126
+ unless reasons.empty?
127
+ # Include the detailed reasons if they fit, otherwise fall back to the minimal
128
+ # marker so the truncation is still signalled.
129
+ detailed = "\"truncated\":#{@format.dump(reasons)}"
130
+ fits = buffer.bytesize + (first ? 0 : 1) + detailed.bytesize + 1 <= @size_limit
131
+
132
+ buffer << "," unless first
133
+ buffer << (fits ? detailed : TRUNCATED)
134
+ end
135
+
136
+ buffer << "}"
137
+
138
+ return buffer
139
+ rescue SystemStackError, StandardError
140
+ return "{#{TRUNCATED}}"
141
+ end
142
+
143
+ # Serialize a single top-level `"key":value` pair, safely handling values that
144
+ # cannot be serialized directly.
145
+ #
146
+ # @parameter key [Object] The field key.
147
+ # @parameter value [Object] The field value.
148
+ # @returns [Array(String, Exception | Nil)] The `"key":value` fragment and the error, if recovery was needed.
149
+ def dump_pair(key, value)
150
+ value_json, error = dump_value(value)
151
+
152
+ return ["#{dump_string(String(key))}:#{value_json}", error]
153
+ end
154
+
155
+ # Serialize a single value, falling back to a safe representation on failure.
156
+ #
157
+ # @parameter value [Object] The value to serialize.
158
+ # @returns [Array(String, Exception | Nil)] The serialized value and the error, if recovery was needed.
159
+ def dump_value(value)
160
+ [@format.dump(value, @depth_limit), nil]
161
+ rescue SystemStackError, StandardError => error
162
+ [@format.dump(safe_dump_recurse(value)), error]
163
+ end
164
+
165
+ # Serialize a string as a JSON string, encoding it safely first.
166
+ #
167
+ # @parameter value [String] The string to serialize.
168
+ # @returns [String] The serialized (quoted) string.
169
+ def dump_string(value)
170
+ @format.dump(value.encode(@encoding, invalid: :replace, undef: :replace))
171
+ end
172
+
38
173
  # Filter the backtrace to remove duplicate frames and reduce verbosity.
39
174
  #
40
175
  # @parameter error [Exception] The exception to filter.
@@ -76,24 +211,16 @@ module Console
76
211
  return frames
77
212
  end
78
213
 
79
- # Dump the given object to a string, replacing it with a safe representation if there is an error.
80
- #
81
- # This is a slow path so we try to avoid it.
214
+ # Build a safe, primitive representation of an error for inclusion as an `"error"` field.
82
215
  #
83
- # @parameter object [Object] The object to dump.
84
216
  # @parameter error [Exception] The error that occurred while dumping the object.
85
- # @returns [Hash] The dumped (truncated) object including error details.
86
- def safe_dump(object, error)
87
- object = safe_dump_recurse(object)
88
-
89
- object[:truncated] = true
90
- object[:error] = {
217
+ # @returns [Hash] The error details (class, message, filtered backtrace).
218
+ def error_info(error)
219
+ {
91
220
  class: safe_dump_recurse(error.class.name),
92
221
  message: safe_dump_recurse(error.message),
93
222
  backtrace: safe_dump_recurse(filter_backtrace(error)),
94
223
  }
95
-
96
- return object
97
224
  end
98
225
 
99
226
  # Create a new hash with identity comparison.
@@ -107,7 +234,7 @@ module Console
107
234
  # @parameter limit [Integer] The maximum depth to recurse into objects.
108
235
  # @parameter objects [Hash] The objects that have already been visited.
109
236
  # @returns [Object] The dumped object as a primitive representation.
110
- def safe_dump_recurse(object, limit = @limit, objects = default_objects)
237
+ def safe_dump_recurse(object, limit = @depth_limit, objects = default_objects)
111
238
  case object
112
239
  when Hash
113
240
  if limit <= 0 || objects[object]
@@ -6,6 +6,7 @@
6
6
  require_relative "format/safe"
7
7
 
8
8
  module Console
9
+ # @namespace
9
10
  module Format
10
11
  # A safe format for converting objects to strings.
11
12
  #
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  module Console
7
- VERSION = "1.35.1"
7
+ VERSION = "1.36.0"
8
8
  end
data/readme.md CHANGED
@@ -34,6 +34,12 @@ Please see the [project documentation](https://socketry.github.io/console/) for
34
34
 
35
35
  Please see the [project releases](https://socketry.github.io/console/releases/index) for all releases.
36
36
 
37
+ ### v1.36.0
38
+
39
+ - Add a `size_limit` to `Console::Format::Safe` (default 16KiB) which rebuilds oversized records field-by-field, keeping as many top-level fields as fit within the limit.
40
+ - Degraded fields are recorded in a `truncated` object that maps each field name to why it was truncated: `true` (dropped for size) or the error (the value could not be serialized directly and a safe representation was kept in its place).
41
+ - Rename `Console::Format::Safe`'s `limit:` to `depth_limit:` (with a deprecated `limit:` alias) to clarify its purpose alongside the new `size_limit:`.
42
+
37
43
  ### v1.35.0
38
44
 
39
45
  - Fix handling of `Errno::ENODEV` errors when calculating the width of a terminal that was been re-opened to `File::NULL`.
@@ -74,12 +80,6 @@ Please see the [project releases](https://socketry.github.io/console/releases/in
74
80
 
75
81
  - Fix logging `exception:` keyword argument when the value was not an exception.
76
82
 
77
- ### v1.29.0
78
-
79
- - Don't make `Kernel#warn` redirection to `Console.warn` the default behavior, you must `require 'console/warn'` to enable it.
80
- - Remove deprecated `Console::Logger#failure`.
81
- - [Consistent handling of exceptions.](https://socketry.github.io/console/releases/index#consistent-handling-of-exceptions.)
82
-
83
83
  ## Contributing
84
84
 
85
85
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Releases
2
2
 
3
+ ## v1.36.0
4
+
5
+ - Add a `size_limit` to `Console::Format::Safe` (default 16KiB) which rebuilds oversized records field-by-field, keeping as many top-level fields as fit within the limit.
6
+ - Degraded fields are recorded in a `truncated` object that maps each field name to why it was truncated: `true` (dropped for size) or the error (the value could not be serialized directly and a safe representation was kept in its place).
7
+ - Rename `Console::Format::Safe`'s `limit:` to `depth_limit:` (with a deprecated `limit:` alias) to clarify its purpose alongside the new `size_limit:`.
8
+
3
9
  ## v1.35.0
4
10
 
5
11
  - Fix handling of `Errno::ENODEV` errors when calculating the width of a terminal that was been re-opened to `File::NULL`.
data.tar.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- S�Ll�< ��|~\@T�ޙ���C����c< ���+xY�2 ��jFp ��ԁ3j0���]B���������Ҡ��mzZR�eE�*f/�_g�SoeN��p:���F��j���a,��" �V��1K�C]r?�yj����lU�a��%q9N6�+C��D誃��l܃�p ��a@��1�
2
- �N���� I�5�e�&�i��k��x���}�� �Քb+�d_>e� 6l�mчw@����C�b"3k(�N��T��:D�p'5�����sJ�^�T��LQ��Z{m�3���^%Ԙ8,"\���r�M(X�v �2l�NڧEӶ�jʹ�Z9�մ��:���L��o���w"��3� hR�~�E�HF�f� ��S
1
+ :}��CS4��<\i�&�� <ք��P��]�ן��:�C��3l����|Q���'���]G
2
+ �[�����
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: console
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.35.1
4
+ version: 1.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -163,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
163
  - !ruby/object:Gem::Version
164
164
  version: '0'
165
165
  requirements: []
166
- rubygems_version: 4.0.6
166
+ rubygems_version: 4.0.10
167
167
  specification_version: 4
168
168
  summary: Beautiful logging for Ruby.
169
169
  test_files: []
metadata.gz.sig CHANGED
Binary file