leto 2.2.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -0
- data/README.md +83 -32
- data/lib/leto/call.rb +45 -5
- data/lib/leto/features.rb +12 -0
- data/lib/leto/utils.rb +7 -1
- data/lib/leto/version.rb +1 -1
- data/lib/leto.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0297fbb39b4e2a4a72e314dde94542722e1f147cb7988181cc6a79529123474
|
|
4
|
+
data.tar.gz: afb6e6670df5f8e0f2ea10630c184d69f676457b0f78431a52944c3c6dff1d44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afe909e7a3e5a99e2fd580f74b709a746b1cdf66c8e8c3c0d61569c2c49faa06accd3c5a628ab6e1f57d42a70a676a851a9f7ba5edbc66132456de84cac9bf93
|
|
7
|
+
data.tar.gz: 1071b59853ec209240c7a298310c548d498af3f8086666623b4911684764975c8905291aae5d711d5be8b43b33382910cfdfb52eab8c10d0414caa5b0e592051
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
12
|
+
## [2.3.0] - 2026-09-29
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- support for Ruby 4's native `Set` class
|
|
17
|
+
|
|
3
18
|
## [2.2.0] - 2026-09-03
|
|
4
19
|
|
|
5
20
|
### Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -4,17 +4,13 @@
|
|
|
4
4
|
[](https://github.com/jaynetics/leto/actions)
|
|
5
5
|
[](https://codecov.io/gh/jaynetics/leto)
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
9
|
+
It takes an object and recursively yields:
|
|
10
10
|
|
|
11
11
|
- the given object
|
|
12
12
|
- instance variables, class variables, constants
|
|
13
|
-
-
|
|
14
|
-
- Enumerable members
|
|
15
|
-
- Struct members
|
|
16
|
-
- [Data](https://docs.ruby-lang.org/en/3.2/Data.html) members
|
|
17
|
-
- Range begins and ends
|
|
13
|
+
- any object attributes, such as Array members, that are built into Ruby ([full list](#built-ins))
|
|
18
14
|
|
|
19
15
|
This makes stuff like deep-freezing fairly easy to implement:
|
|
20
16
|
|
|
@@ -28,7 +24,7 @@ my_object[0].frozen? # => true
|
|
|
28
24
|
my_object[0][:a][1].begin.frozen? # => true
|
|
29
25
|
```
|
|
30
26
|
|
|
31
|
-
Note: the slightly smarter `Leto.deep_freeze` is one of the [included utility methods](#included-
|
|
27
|
+
Note: the slightly smarter `Leto.deep_freeze` is one of the [included utility methods](#included-utils).
|
|
32
28
|
|
|
33
29
|
## Usage
|
|
34
30
|
|
|
@@ -37,51 +33,93 @@ Basic example:
|
|
|
37
33
|
```ruby
|
|
38
34
|
object = [{ a: ['b', 'c'..'d'] }]
|
|
39
35
|
|
|
40
|
-
Leto.call(object) { |el|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
Leto.call(object) { |el| puts "#{el.class}: #{el}" }
|
|
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
|
|
50
|
+
```
|
|
51
51
|
|
|
52
|
+
Calls without a block return an `Enumerator`:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
52
55
|
Leto.call(object).to_a
|
|
53
56
|
# => [[{:a=>["b", ["c".."d"]]}], {:a=>["b", ["c".."d"]]}, :a, ...]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Leto.trace` behaves like `.call`, but also yields each (sub-)object's `Path`:
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
```ruby
|
|
56
62
|
Leto.trace(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Paths can be looked up with `Leto::Path#resolve` or `Leto.dig`:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
69
81
|
path = Leto.trace(object).map { |_el, path| path }.last # => #<Leto::Path...>
|
|
70
82
|
path.resolve # => "d"
|
|
71
83
|
Leto.dig(object, path) # => "d"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Arrays of Arrays, where the inner ones contain method names and arguments, are valid paths:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
72
89
|
Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
|
|
73
90
|
```
|
|
74
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
|
+
|
|
75
111
|
### Included utility methods
|
|
76
112
|
|
|
77
113
|
- [`Leto.deep_freeze(obj)`](https://github.com/search?q=deep_freeze+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
|
78
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)
|
|
79
116
|
- [`Leto.deep_print(obj)`](https://github.com/search?q=deep_print+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
|
80
117
|
- for debugging - prints more information than `pretty_print` does by default
|
|
81
118
|
- [`Leto.deep_eql?(obj1, obj2)`](https://github.com/search?q=deep_eql+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
|
82
119
|
- stricter version of `#eql?` that takes all ivars into consideration
|
|
83
120
|
- [`Leto.deep_dup(obj)`](https://github.com/search?q=deep_dup+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
|
84
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
|
|
85
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)
|
|
86
124
|
- useful for debugging or verifying that a `#dup` implementation is sane
|
|
87
125
|
- [`Leto.shared_mutables(obj1, obj2)`](https://github.com/search?q=shared_mutables+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
|
@@ -89,6 +127,19 @@ Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
|
|
|
89
127
|
- [`Leto.shared_objects(obj1, obj2)`](https://github.com/search?q=shared_objects+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
|
90
128
|
- returns all objects shared by `obj1` and `obj2`, whether mutable or not
|
|
91
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
|
+
|
|
92
143
|
## Benchmarks
|
|
93
144
|
|
|
94
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(
|
|
@@ -60,12 +64,19 @@ module Leto
|
|
|
60
64
|
obj.members.each do |member|
|
|
61
65
|
traverse(obj[member], path&.+([[:[], member]]), depth, max_depth, seen, block)
|
|
62
66
|
end
|
|
63
|
-
when Enumerable
|
|
64
|
-
obj.each_with_index do |el, idx|
|
|
65
|
-
traverse(el, path&.+([[:[], idx]]), depth, max_depth, seen, block)
|
|
66
|
-
end
|
|
67
67
|
#{
|
|
68
|
-
|
|
68
|
+
set_feature? && <<-SET_FEATURE_RUBY
|
|
69
|
+
when Set
|
|
70
|
+
obj.each_with_index do |el, idx|
|
|
71
|
+
traverse(
|
|
72
|
+
el, path&.+([[:to_a], [:[], idx]]),
|
|
73
|
+
depth, max_depth, seen, block
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
SET_FEATURE_RUBY
|
|
77
|
+
}
|
|
78
|
+
#{
|
|
79
|
+
data_feature? && <<-DATA_FEATURE_RUBY
|
|
69
80
|
when Data
|
|
70
81
|
obj.members.each do |member|
|
|
71
82
|
traverse(
|
|
@@ -76,6 +87,35 @@ module Leto
|
|
|
76
87
|
end
|
|
77
88
|
DATA_FEATURE_RUBY
|
|
78
89
|
}
|
|
90
|
+
when Enumerable
|
|
91
|
+
obj.each_with_index do |el, idx|
|
|
92
|
+
traverse(el, path&.+([[:[], idx]]), depth, max_depth, seen, block)
|
|
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)
|
|
79
119
|
end
|
|
80
120
|
end
|
|
81
121
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# feature detection code for ruby
|
|
2
|
+
module Leto
|
|
3
|
+
def self.data_feature?
|
|
4
|
+
defined?(Data) && Data.respond_to?(:define)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Pre-Ruby-4.0, Hash-backed Sets are traversed through their
|
|
8
|
+
# @hash instance variable and don't need custom handling.
|
|
9
|
+
def self.set_feature?
|
|
10
|
+
defined?(Set) && Set.respond_to?(:[]) && !Set[].instance_variable_defined?(:@hash)
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/leto/utils.rb
CHANGED
|
@@ -34,8 +34,14 @@ module Leto
|
|
|
34
34
|
copy[*args] = deep_dup(el, include_modules: include_modules)
|
|
35
35
|
when :send # Data
|
|
36
36
|
copy = copy.with(args[0] => deep_dup(el, include_modules: include_modules))
|
|
37
|
+
when :to_a # Set
|
|
38
|
+
copy = (copy - [el]) + [deep_dup(el, include_modules: include_modules)]
|
|
37
39
|
when :begin
|
|
38
|
-
return Range.new(
|
|
40
|
+
return Range.new(
|
|
41
|
+
deep_dup(obj.begin, include_modules: include_modules),
|
|
42
|
+
deep_dup(obj.end, include_modules: include_modules),
|
|
43
|
+
obj.exclude_end?,
|
|
44
|
+
)
|
|
39
45
|
end
|
|
40
46
|
end
|
|
41
47
|
|
data/lib/leto/version.rb
CHANGED
data/lib/leto.rb
CHANGED
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.
|
|
4
|
+
version: 2.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Janosch Müller
|
|
@@ -27,6 +27,7 @@ files:
|
|
|
27
27
|
- lib/leto.rb
|
|
28
28
|
- lib/leto/call.rb
|
|
29
29
|
- lib/leto/dig.rb
|
|
30
|
+
- lib/leto/features.rb
|
|
30
31
|
- lib/leto/path.rb
|
|
31
32
|
- lib/leto/utils.rb
|
|
32
33
|
- lib/leto/version.rb
|
|
@@ -50,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
50
51
|
- !ruby/object:Gem::Version
|
|
51
52
|
version: '0'
|
|
52
53
|
requirements: []
|
|
53
|
-
rubygems_version: 4.0.
|
|
54
|
+
rubygems_version: 4.0.23
|
|
54
55
|
specification_version: 4
|
|
55
56
|
summary: Generic object traverser
|
|
56
57
|
test_files: []
|