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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0362bb08490e1bd600bb8996a1be449584295433c047472864578b2d43a6cb5
4
- data.tar.gz: de8244c6e7d6bd2a58453effbadf1e93f9956a0aace1fc0fcef1c97660e486f4
3
+ metadata.gz: f0297fbb39b4e2a4a72e314dde94542722e1f147cb7988181cc6a79529123474
4
+ data.tar.gz: afb6e6670df5f8e0f2ea10630c184d69f676457b0f78431a52944c3c6dff1d44
5
5
  SHA512:
6
- metadata.gz: a8a4a40c37e53158ce69775451a56a2b939cecb01df5c8441a0c7a18b9e4c88db68d350dd57abbc2d87fd03c770335f2e92344e2fba4370280796bb46fce1d6e
7
- data.tar.gz: b73106dde5c5f19b65c6b95ac6f0fdf4039ab84eaf6dfb53abe705e54e4b7c2f8951fd3ebe205cf53ca46faaccf3812cc1fc2e534979a8adfd99df07bc0a66fb
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
@@ -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,17 +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
- - 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-utility-methods).
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| p el }
41
- # prints:
42
- #
43
- # [{:a=>["b", ["c".."d"]]}]
44
- # {:a=>["b", ["c".."d"]]}
45
- # :a
46
- # ["b", ["c".."d"]]
47
- # "b"
48
- # "c".."d"
49
- # "c"
50
- # "d"
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
- # Leto::trace behaves like ::call, but also yields each (sub-)object's path:
61
+ ```ruby
56
62
  Leto.trace(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
57
- # prints:
58
- #
59
- # [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
60
- # {:a=>["b", "c".."d"]} @#<Leto::Path [{:a=>["b", "c".."d"]}][0]>
61
- # :a @#<Leto::Path [{:a=>["b", "c".."d"]}][0].keys[0]>
62
- # ["b", "c".."d"] @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a]>
63
- # "b" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][0]>
64
- # "c".."d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1]>
65
- # "c" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].begin>
66
- # "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
67
-
68
- # paths can be looked up with Leto::Path#resolve or Leto::dig
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
- defined?(Data) && Data.respond_to?(:define) && <<-DATA_FEATURE_RUBY
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(deep_dup(obj.begin), deep_dup(obj.end), obj.exclude_end?)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leto
4
- VERSION = "2.2.0"
4
+ VERSION = "2.4.0"
5
5
  end
data/lib/leto.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'leto/features'
3
4
  require_relative 'leto/call'
4
5
  require_relative 'leto/dig'
5
6
  require_relative 'leto/path'
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.2.0
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.3
54
+ rubygems_version: 4.0.23
54
55
  specification_version: 4
55
56
  summary: Generic object traverser
56
57
  test_files: []