leto 2.3.0 → 2.4.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: 7a4c2fdd688142eb3f9db1a5d0d10cad624225413f6892a65eeef1a0aec3df00
4
- data.tar.gz: 7ee194719f3912044b675f874f60d42bf4c6e7c60be5e83318cf3f2aaeca6225
3
+ metadata.gz: f0297fbb39b4e2a4a72e314dde94542722e1f147cb7988181cc6a79529123474
4
+ data.tar.gz: afb6e6670df5f8e0f2ea10630c184d69f676457b0f78431a52944c3c6dff1d44
5
5
  SHA512:
6
- metadata.gz: 4edd7d0d8677b548585517b499a57ae630b5326053192f7b10b6de101c8899aab46d956de787df0a480cf66f73fd34c5da3518db2f1c61abc82b7758cdef7cbe
7
- data.tar.gz: c3678597e116adf629f4229193c921a003e1f270030c2d076464af427806b76ce1560f6726a231eca7507af334be457e91f2c286d1bc2d0349d66696f73c25d8
6
+ metadata.gz: afe909e7a3e5a99e2fd580f74b709a746b1cdf66c8e8c3c0d61569c2c49faa06accd3c5a628ab6e1f57d42a70a676a851a9f7ba5edbc66132456de84cac9bf93
7
+ data.tar.gz: 1071b59853ec209240c7a298310c548d498af3f8086666623b4911684764975c8905291aae5d711d5be8b43b33382910cfdfb52eab8c10d0414caa5b0e592051
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.4.0] - 2026-10-26
4
+
5
+ ### Added
6
+
7
+ - support for `Complex` and `Rational` objects
8
+ - support for `Hash#default` and `Hash#default_proc`
9
+ - support for `Exception` fields
10
+ - support for `Time` zones
11
+
3
12
  ## [2.3.0] - 2026-09-29
4
13
 
5
14
  ### Added
data/Gemfile CHANGED
@@ -5,6 +5,8 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in leto.gemspec
6
6
  gemspec
7
7
 
8
+ gem "irb"
9
+ gem "ostruct"
8
10
  gem "rake", "~> 13.0"
9
11
  gem "rspec", "~> 3.0"
10
12
 
data/README.md CHANGED
@@ -4,18 +4,13 @@
4
4
  [![Build Status](https://github.com/jaynetics/leto/actions/workflows/tests.yml/badge.svg)](https://github.com/jaynetics/leto/actions)
5
5
  [![Coverage](https://codecov.io/gh/jaynetics/leto/branch/main/graph/badge.svg?token=0993K9I8VC)](https://codecov.io/gh/jaynetics/leto)
6
6
 
7
- A generic object traverser for Ruby (named after the Greek [childbearing goddess Leto](https://www.theoi.com/Titan/TitanisLeto.html)).
7
+ Leto is a generic object traverser for Ruby (named after the Greek [childbearing goddess Leto](https://www.theoi.com/Titan/TitanisLeto.html)).
8
8
 
9
- Takes an object and recursively yields:
9
+ It takes an object and recursively yields:
10
10
 
11
11
  - the given object
12
12
  - instance variables, class variables, constants
13
- - Hash keys and values
14
- - Enumerable members
15
- - Range begins and ends
16
- - Struct members
17
- - [Data](https://docs.ruby-lang.org/en/3.2/Data.html) members
18
- - Set members (for [native Sets on Ruby >= 4.0](https://bugs.ruby-lang.org/issues/21216))
13
+ - any object attributes, such as Array members, that are built into Ruby ([full list](#built-ins))
19
14
 
20
15
  This makes stuff like deep-freezing fairly easy to implement:
21
16
 
@@ -29,7 +24,7 @@ my_object[0].frozen? # => true
29
24
  my_object[0][:a][1].begin.frozen? # => true
30
25
  ```
31
26
 
32
- Note: the slightly smarter `Leto.deep_freeze` is one of the [included utility methods](#included-utility-methods).
27
+ Note: the slightly smarter `Leto.deep_freeze` is one of the [included utility methods](#included-utils).
33
28
 
34
29
  ## Usage
35
30
 
@@ -39,16 +34,19 @@ Basic example:
39
34
  object = [{ a: ['b', 'c'..'d'] }]
40
35
 
41
36
  Leto.call(object) { |el| puts "#{el.class}: #{el}" }
42
- # prints:
43
- #
44
- # Array: [{a: ["b", "c".."d"]}]
45
- # Hash: {a: ["b", "c".."d"]}
46
- # Symbol: a
47
- # Array: ["b", "c".."d"]
48
- # String: b
49
- # Range: c..d
50
- # String: c
51
- # String: d
37
+ ```
38
+
39
+ This prints:
40
+
41
+ ```
42
+ Array: [{a: ["b", "c".."d"]}]
43
+ Hash: {a: ["b", "c".."d"]}
44
+ Symbol: a
45
+ Array: ["b", "c".."d"]
46
+ String: b
47
+ Range: c..d
48
+ String: c
49
+ String: d
52
50
  ```
53
51
 
54
52
  Calls without a block return an `Enumerator`:
@@ -62,16 +60,19 @@ Leto.call(object).to_a
62
60
 
63
61
  ```ruby
64
62
  Leto.trace(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
65
- # prints:
66
- #
67
- # [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
68
- # {:a=>["b", "c".."d"]} @#<Leto::Path [{:a=>["b", "c".."d"]}][0]>
69
- # :a @#<Leto::Path [{:a=>["b", "c".."d"]}][0].keys[0]>
70
- # ["b", "c".."d"] @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a]>
71
- # "b" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][0]>
72
- # "c".."d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1]>
73
- # "c" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].begin>
74
- # "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
63
+ ```
64
+
65
+ This prints:
66
+
67
+ ```
68
+ [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
69
+ {:a=>["b", "c".."d"]} @#<Leto::Path [{:a=>["b", "c".."d"]}][0]>
70
+ :a @#<Leto::Path [{:a=>["b", "c".."d"]}][0].keys[0]>
71
+ ["b", "c".."d"] @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a]>
72
+ "b" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][0]>
73
+ "c".."d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1]>
74
+ "c" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].begin>
75
+ "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
75
76
  ```
76
77
 
77
78
  Paths can be looked up with `Leto::Path#resolve` or `Leto.dig`:
@@ -82,22 +83,43 @@ path.resolve # => "d"
82
83
  Leto.dig(object, path) # => "d"
83
84
  ```
84
85
 
85
- Arrays of Arrays, where the latter ones contain method names and arguments, are valid paths:
86
+ Arrays of Arrays, where the inner ones contain method names and arguments, are valid paths:
86
87
 
87
88
  ```ruby
88
89
  Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
89
90
  ```
90
91
 
92
+ <a name='built-ins'></a>
93
+
94
+ ### Supported built-in classes and attributes
95
+
96
+ The following classes are built into Ruby and some of their attributes are not exposed as regular instance variables. Leto handles these explicitly.
97
+
98
+ - `Complex` objects' real and imaginary parts
99
+ - [`Data`](https://docs.ruby-lang.org/en/3.2/Data.html) members
100
+ - `Enumerable` members
101
+ - `Exception` messages, backtraces, causes, and various subclass fields
102
+ - `Hash` keys, values, defaults, and default_procs
103
+ - `Range` begins and ends
104
+ - `Rational` numerators and denominators
105
+ - `Set` members (including for [native Sets on Ruby >= 4.0](https://bugs.ruby-lang.org/issues/21216))
106
+ - `Struct` and `OpenStruct` members
107
+ - `Time` zones, including custom timezone objects
108
+
109
+ <a name='included-utils'></a>
110
+
91
111
  ### Included utility methods
92
112
 
93
113
  - [`Leto.deep_freeze(obj)`](https://github.com/search?q=deep_freeze+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
94
114
  - similar to the version above, but avoids freezing Modules and unfreezables
115
+ - similar to [`Ractor.make_shareable`](https://docs.ruby-lang.org/en/4.0/Ractor.html#method-c-make_shareable)
95
116
  - [`Leto.deep_print(obj)`](https://github.com/search?q=deep_print+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
96
117
  - for debugging - prints more information than `pretty_print` does by default
97
118
  - [`Leto.deep_eql?(obj1, obj2)`](https://github.com/search?q=deep_eql+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
98
119
  - stricter version of `#eql?` that takes all ivars into consideration
99
120
  - [`Leto.deep_dup(obj)`](https://github.com/search?q=deep_dup+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
100
121
  - more thorough than [`active_support`](https://www.rubydoc.info/search/gems/activesupport?q=deep_dup) or [`deep_dup`](https://github.com/ollie/deep_dup) gems, e.g. dups ivars
122
+ - similar to [`Ractor.make_shareable(obj, copy: true)`](https://docs.ruby-lang.org/en/4.0/Ractor.html#method-c-make_shareable), but doesn't freeze
101
123
  - [`Leto.shared_mutable_state?(obj1, obj2, ...)`](https://github.com/search?q=shared_mutable_state+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
102
124
  - useful for debugging or verifying that a `#dup` implementation is sane
103
125
  - [`Leto.shared_mutables(obj1, obj2)`](https://github.com/search?q=shared_mutables+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
@@ -105,6 +127,19 @@ Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
105
127
  - [`Leto.shared_objects(obj1, obj2)`](https://github.com/search?q=shared_objects+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
106
128
  - returns all objects shared by `obj1` and `obj2`, whether mutable or not
107
129
 
130
+ To make a shareable copy while leaving the original mutable:
131
+
132
+ ```ruby
133
+ copy = Leto.deep_freeze(object, shareable: true, copy: true)
134
+ ```
135
+
136
+ Duplicating a Proc does not duplicate its captured environment. Shareable freezing
137
+ uses Ruby's rules for Procs, Methods, and other native objects, which vary between
138
+ Ruby versions.
139
+
140
+ Shareable freezing leaves Classes and Modules as they are, following Ractor's
141
+ rules. `include_modules: true` applies to regular deep freezing.
142
+
108
143
  ## Benchmarks
109
144
 
110
145
  ```
data/lib/leto/call.rb CHANGED
@@ -38,6 +38,10 @@ module Leto
38
38
  traverse(k, path&.+([[:keys], [:[], i]]), depth, max_depth, seen, block)
39
39
  traverse(obj[k], path&.+([[:[], k]]), depth, max_depth, seen, block)
40
40
  end
41
+ obj.default.nil? ||
42
+ traverse(obj.default, path&.+([[:default]]), depth, max_depth, seen, block)
43
+ obj.default_proc.nil? ||
44
+ traverse(obj.default_proc, path&.+([[:default_proc]]), depth, max_depth, seen, block)
41
45
  when Module
42
46
  obj.class_variables.each do |cvar_name|
43
47
  traverse(
@@ -87,6 +91,31 @@ module Leto
87
91
  obj.each_with_index do |el, idx|
88
92
  traverse(el, path&.+([[:[], idx]]), depth, max_depth, seen, block)
89
93
  end
94
+ when Complex
95
+ traverse(obj.real, path&.+([[:real]]), depth, max_depth, seen, block)
96
+ traverse(obj.imaginary, path&.+([[:imaginary]]), depth, max_depth, seen, block)
97
+ when Rational
98
+ traverse(obj.numerator, path&.+([[:numerator]]), depth, max_depth, seen, block)
99
+ traverse(obj.denominator, path&.+([[:denominator]]), depth, max_depth, seen, block)
100
+ when Exception
101
+ %i[
102
+ message backtrace backtrace_locations cause name receiver args key
103
+ matchee result exit_value reason tag value path status signo errno
104
+ ].each do |attr|
105
+ attr = :original_message if attr == :message && obj.respond_to?(:original_message)
106
+ begin
107
+ value = obj.send(attr)
108
+ rescue ArgumentError
109
+ # Some exceptions have no receiver or key.
110
+ raise unless %i[receiver key].include?(attr)
111
+ rescue NoMethodError # ignore
112
+ end
113
+ value.nil? ||
114
+ traverse(value, path&.+([[attr]]), depth, max_depth, seen, block)
115
+ end
116
+ when Time
117
+ obj.zone.nil? ||
118
+ traverse(obj.zone, path&.+([[:zone]]), depth, max_depth, seen, block)
90
119
  end
91
120
  end
92
121
 
data/lib/leto/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leto
4
- VERSION = "2.3.0"
4
+ VERSION = "2.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leto
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janosch Müller
@@ -51,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  - !ruby/object:Gem::Version
52
52
  version: '0'
53
53
  requirements: []
54
- rubygems_version: 4.0.20
54
+ rubygems_version: 4.0.23
55
55
  specification_version: 4
56
56
  summary: Generic object traverser
57
57
  test_files: []