leto 1.0.0 → 2.1.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/.rubocop.yml +6 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +5 -4
- data/README.md +14 -10
- data/Rakefile +7 -5
- data/leto.gemspec +1 -1
- data/lib/leto/call.rb +77 -49
- data/lib/leto/path.rb +5 -2
- data/lib/leto/utils.rb +54 -9
- data/lib/leto/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ed0d190b234b4976a219ba5b50a3e9cb4f394be7b1a27aa2f1d7bdfe93887b0
|
4
|
+
data.tar.gz: eafafd24510514cf7ba4e59cdf1a967c7464d175b8ceed1f3cafa2e78fffcc1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79b0be7884c86be4a14977e5aaae6147da85b857f632013bd82f9fe95addf28a4014ea7adeba1c75dfc63b9988ab7426c622cb79bc2c2886dca04be246ee2db5
|
7
|
+
data.tar.gz: 961a19be85b69aac0763828648c09100af4a9f84eff07a45e2b9c0c247c84e5b7ae9647a9bd4a7de791eb7cd0f0342b91f82d2cd50a85286425e4a8f915bd2fe
|
data/.rubocop.yml
CHANGED
@@ -21,8 +21,14 @@ Lint/AmbiguousBlockAssociation:
|
|
21
21
|
Lint/AmbiguousOperatorPrecedence:
|
22
22
|
Enabled: false
|
23
23
|
|
24
|
+
Style/DocumentDynamicEvalDefinition:
|
25
|
+
Enabled: false
|
26
|
+
|
24
27
|
Style/FetchEnvVar:
|
25
28
|
Enabled: false
|
26
29
|
|
27
30
|
Style/FrozenStringLiteralComment:
|
28
31
|
Enabled: false
|
32
|
+
|
33
|
+
Style/SymbolProc:
|
34
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [2.1.0] - 2023-05-20
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- `::call`, `::trace` and `::deep_dup` support for Data feature of Ruby 3.2
|
8
|
+
|
9
|
+
## [2.0.0] - 2023-02-27
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- removed support for getting paths by calling `::call` with a two-arg block
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- `::deep_dup`
|
18
|
+
- `::trace`
|
19
|
+
- support for older Rubies
|
20
|
+
|
3
21
|
## [1.0.0] - 2023-02-24
|
4
22
|
|
5
23
|
- Initial release
|
data/Gemfile
CHANGED
@@ -6,9 +6,10 @@ source "https://rubygems.org"
|
|
6
6
|
gemspec
|
7
7
|
|
8
8
|
gem "rake", "~> 13.0"
|
9
|
-
|
10
9
|
gem "rspec", "~> 3.0"
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
gem "
|
11
|
+
if RUBY_VERSION.to_f >= 3.0
|
12
|
+
gem "relaxed-rubocop"
|
13
|
+
gem "rubocop", "~> 1.21"
|
14
|
+
gem "simplecov-cobertura"
|
15
|
+
end
|
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# Leto
|
2
2
|
|
3
3
|
[](http://badge.fury.io/rb/leto)
|
4
|
-
[](https://github.com/jaynetics/leto/actions)
|
4
|
+
[](https://github.com/jaynetics/leto/actions)
|
5
|
+
[](https://codecov.io/gh/jaynetics/leto)
|
5
6
|
|
6
7
|
A generic object traverser for Ruby (named after the Greek [childbearing goddess Leto](https://www.theoi.com/Titan/TitanisLeto.html)).
|
7
8
|
|
@@ -12,6 +13,7 @@ Takes an object and recursively yields:
|
|
12
13
|
- Hash keys and values
|
13
14
|
- Enumerable members
|
14
15
|
- Struct members
|
16
|
+
- [Data](https://docs.ruby-lang.org/en/3.2/Data.html) members
|
15
17
|
- Range begins and ends
|
16
18
|
|
17
19
|
This makes stuff like deep-freezing fairly easy to implement:
|
@@ -50,8 +52,8 @@ Leto.call(object) { |el| p el }
|
|
50
52
|
Leto.call(object).to_a
|
51
53
|
# => [[{:a=>["b", ["c".."d"]]}], {:a=>["b", ["c".."d"]]}, :a, ...]
|
52
54
|
|
53
|
-
#
|
54
|
-
Leto.
|
55
|
+
# Leto::trace behaves like ::call, but also yields each (sub-)object's path:
|
56
|
+
Leto.trace(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
|
55
57
|
# prints:
|
56
58
|
#
|
57
59
|
# [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
|
@@ -64,7 +66,7 @@ Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
|
|
64
66
|
# "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
|
65
67
|
|
66
68
|
# paths can be looked up with Leto::Path#resolve or Leto::dig
|
67
|
-
path = Leto.
|
69
|
+
path = Leto.trace(object).map { |_el, path| path }.last # => #<Leto::Path...>
|
68
70
|
path.resolve # => "d"
|
69
71
|
Leto.dig(object, path) # => "d"
|
70
72
|
Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
|
@@ -72,17 +74,19 @@ Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
|
|
72
74
|
|
73
75
|
### Included utility methods
|
74
76
|
|
75
|
-
- `Leto.deep_freeze(obj)`
|
77
|
+
- [`Leto.deep_freeze(obj)`](https://github.com/search?q=deep_freeze+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
76
78
|
- similar to the version above, but avoids freezing Modules and unfreezables
|
77
|
-
- `Leto.deep_print(obj)`
|
79
|
+
- [`Leto.deep_print(obj)`](https://github.com/search?q=deep_print+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
78
80
|
- for debugging - prints more information than `pretty_print` does by default
|
79
|
-
- `Leto.deep_eql?(obj1, obj2)`
|
81
|
+
- [`Leto.deep_eql?(obj1, obj2)`](https://github.com/search?q=deep_eql+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
80
82
|
- stricter version of `#eql?` that takes all ivars into consideration
|
81
|
-
- `Leto.
|
83
|
+
- [`Leto.deep_dup(obj)`](https://github.com/search?q=deep_dup+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
84
|
+
- 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
|
85
|
+
- [`Leto.shared_mutable_state?(obj1, obj2)`](https://github.com/search?q=shared_mutable_state+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
82
86
|
- useful for debugging or verifying that a `#dup` implementation is sane
|
83
|
-
- `Leto.shared_mutables(obj1, obj2)`
|
87
|
+
- [`Leto.shared_mutables(obj1, obj2)`](https://github.com/search?q=shared_mutables+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
84
88
|
- useful for debugging or verifying that a `#dup` implementation is sane
|
85
|
-
- `Leto.shared_objects(obj1, obj2)`
|
89
|
+
- [`Leto.shared_objects(obj1, obj2)`](https://github.com/search?q=shared_objects+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
|
86
90
|
- returns all objects shared by `obj1` and `obj2`, whether mutable or not
|
87
91
|
|
88
92
|
## Benchmarks
|
data/Rakefile
CHANGED
@@ -5,8 +5,10 @@ require "rspec/core/rake_task"
|
|
5
5
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
RuboCop::RakeTask.new
|
11
|
-
|
12
|
-
|
8
|
+
if RUBY_VERSION.to_f >= 3.0
|
9
|
+
require "rubocop/rake_task"
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
task default: %i[spec rubocop]
|
12
|
+
else
|
13
|
+
task default: %i[spec]
|
14
|
+
end
|
data/leto.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.summary = "Generic object traverser"
|
12
12
|
spec.homepage = "https://github.com/jaynetics/leto"
|
13
13
|
spec.license = "MIT"
|
14
|
-
spec.required_ruby_version = ">= 2.
|
14
|
+
spec.required_ruby_version = ">= 2.3.0"
|
15
15
|
|
16
16
|
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
data/lib/leto/call.rb
CHANGED
@@ -1,64 +1,92 @@
|
|
1
1
|
module Leto
|
2
|
-
|
2
|
+
def self.call(obj, max_depth: nil, path: nil, &block)
|
3
|
+
block_given? or return enum_for(__method__, obj, max_depth: max_depth, path: path)
|
3
4
|
|
4
|
-
|
5
|
-
return enum_for(__method__, obj) unless block_given?
|
6
|
-
|
7
|
-
seen = {}.tap(&:compare_by_identity)
|
8
|
-
seen[Process::Tms] = true if LEAKY_PROCESS_TMS
|
9
|
-
path = block.arity == 2 ? Path.new(start: obj) : nil
|
10
|
-
traverse(obj, path, seen, block)
|
5
|
+
traverse(obj, path, 0, max_depth, build_seen_hash, block)
|
11
6
|
obj
|
12
7
|
end
|
13
8
|
|
14
|
-
def self.
|
15
|
-
return
|
9
|
+
def self.trace(obj, max_depth: nil, path: nil, &block)
|
10
|
+
block_given? or return enum_for(__method__, obj, max_depth: max_depth, path: path)
|
16
11
|
|
17
|
-
|
12
|
+
call(obj, max_depth: max_depth, path: path || Path.new(start: obj), &block)
|
13
|
+
end
|
18
14
|
|
19
|
-
|
15
|
+
instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
20
16
|
|
21
|
-
|
22
|
-
traverse(
|
23
|
-
obj.instance_variable_get(ivar_name),
|
24
|
-
path&.+([[:instance_variable_get, ivar_name]]),
|
25
|
-
seen, block
|
26
|
-
)
|
27
|
-
end
|
17
|
+
private
|
28
18
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
obj.class_variable_get(cvar_name),
|
39
|
-
path&.+([[:class_variable_get, cvar_name]]),
|
40
|
-
seen, block
|
41
|
-
)
|
42
|
-
end
|
43
|
-
obj.constants.each do |const_name|
|
19
|
+
def traverse(obj, path, depth, max_depth, seen, block)
|
20
|
+
return if seen[obj] || max_depth&.<(depth)
|
21
|
+
|
22
|
+
seen[obj] = true
|
23
|
+
depth += 1
|
24
|
+
|
25
|
+
path ? block.call(obj, path) : block.call(obj)
|
26
|
+
|
27
|
+
obj.instance_variables.each do |ivar_name|
|
44
28
|
traverse(
|
45
|
-
obj.
|
46
|
-
path&.+([[:
|
47
|
-
seen, block
|
29
|
+
obj.instance_variable_get(ivar_name),
|
30
|
+
path&.+([[:instance_variable_get, ivar_name]]),
|
31
|
+
depth, max_depth, seen, block
|
48
32
|
)
|
49
33
|
end
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
34
|
+
|
35
|
+
case obj
|
36
|
+
when Hash
|
37
|
+
obj.keys.each_with_index do |k, i|
|
38
|
+
traverse(k, path&.+([[:keys], [:[], i]]), depth, max_depth, seen, block)
|
39
|
+
traverse(obj[k], path&.+([[:[], k]]), depth, max_depth, seen, block)
|
40
|
+
end
|
41
|
+
when Module
|
42
|
+
obj.class_variables.each do |cvar_name|
|
43
|
+
traverse(
|
44
|
+
obj.class_variable_get(cvar_name),
|
45
|
+
path&.+([[:class_variable_get, cvar_name]]),
|
46
|
+
depth, max_depth, seen, block
|
47
|
+
)
|
48
|
+
end
|
49
|
+
obj.constants.each do |const_name|
|
50
|
+
traverse(
|
51
|
+
obj.const_get(const_name),
|
52
|
+
path&.+([[:const_get, const_name]]),
|
53
|
+
depth, max_depth, seen, block
|
54
|
+
)
|
55
|
+
end
|
56
|
+
when Range
|
57
|
+
traverse(obj.begin, path&.+([[:begin]]), depth, max_depth, seen, block)
|
58
|
+
traverse(obj.end, path&.+([[:end]]), depth, max_depth, seen, block)
|
59
|
+
when Struct
|
60
|
+
obj.members.each do |member|
|
61
|
+
traverse(obj[member], path&.+([[:[], member]]), depth, max_depth, seen, block)
|
62
|
+
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
|
+
#{
|
68
|
+
defined?(Data) && Data.respond_to?(:define) && <<-DATA_FEATURE_RUBY
|
69
|
+
when Data
|
70
|
+
obj.members.each do |member|
|
71
|
+
traverse(
|
72
|
+
obj.send(member),
|
73
|
+
path&.+([[:send, member]]),
|
74
|
+
depth, max_depth, seen, block
|
75
|
+
)
|
76
|
+
end
|
77
|
+
DATA_FEATURE_RUBY
|
78
|
+
}
|
60
79
|
end
|
61
80
|
end
|
62
|
-
|
63
|
-
|
81
|
+
|
82
|
+
# ignore leaky constants in old rubies
|
83
|
+
def build_seen_hash
|
84
|
+
hash = {}
|
85
|
+
hash.compare_by_identity
|
86
|
+
#{'hash[::Etc::Group] = true' if defined?(::Etc::Group)}
|
87
|
+
#{'hash[::Etc::Passwd] = true' if defined?(::Etc::Passwd)}
|
88
|
+
#{'hash[::Process::Tms] = true' if defined?(::Process::Tms)}
|
89
|
+
hash
|
90
|
+
end
|
91
|
+
RUBY
|
64
92
|
end
|
data/lib/leto/path.rb
CHANGED
@@ -14,6 +14,11 @@ module Leto
|
|
14
14
|
steps.each(&block)
|
15
15
|
end
|
16
16
|
|
17
|
+
def size
|
18
|
+
steps.size
|
19
|
+
end
|
20
|
+
alias count size
|
21
|
+
|
17
22
|
def +(other)
|
18
23
|
self.class.new(start: start, steps: steps + other.to_a)
|
19
24
|
end
|
@@ -21,8 +26,6 @@ module Leto
|
|
21
26
|
def resolve
|
22
27
|
steps.inject(start) do |obj, (method, *args)|
|
23
28
|
obj&.send(method, *args) or break obj
|
24
|
-
rescue StandardError => e
|
25
|
-
warn "#{__method__}: #{e.class} #{e.message}"
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
data/lib/leto/utils.rb
CHANGED
@@ -8,7 +8,7 @@ module Leto
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def self.deep_print(obj, print_method: :inspect, indent: 4, show_path: true)
|
11
|
-
|
11
|
+
trace(obj) do |el, path|
|
12
12
|
puts "#{' ' * path.count * indent}#{el.send(print_method)}" \
|
13
13
|
"#{" @ #{path.inspect}" if show_path}" \
|
14
14
|
[0..78]
|
@@ -20,8 +20,31 @@ module Leto
|
|
20
20
|
call(obj1).to_a == call(obj2).to_a
|
21
21
|
end
|
22
22
|
|
23
|
+
def self.deep_dup(obj, include_modules: false)
|
24
|
+
return obj if IMMUTABLE_CLASSES.include?(obj.class) || !duplicable?(obj) ||
|
25
|
+
(!include_modules && obj.is_a?(Module))
|
26
|
+
|
27
|
+
copy = obj.dup
|
28
|
+
|
29
|
+
trace(obj, max_depth: 1).each do |el, path|
|
30
|
+
method, *args = path.steps[0]
|
31
|
+
case method
|
32
|
+
when :instance_variable_get
|
33
|
+
copy.instance_variable_set(*args, deep_dup(el, include_modules: include_modules))
|
34
|
+
when :[]
|
35
|
+
copy[*args] = deep_dup(el, include_modules: include_modules)
|
36
|
+
when :send # Data
|
37
|
+
copy = copy.with(args[0] => deep_dup(el, include_modules: include_modules))
|
38
|
+
when :begin
|
39
|
+
return Range.new(deep_dup(obj.begin), deep_dup(obj.end), obj.exclude_end?)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
copy
|
44
|
+
end
|
45
|
+
|
23
46
|
def self.shared_mutable_state?(obj1, obj2)
|
24
|
-
|
47
|
+
each_shared_object(obj1, obj2, filter: method(:mutable?)).any?
|
25
48
|
end
|
26
49
|
|
27
50
|
# returns [[shared_object, path1, path2], ...], e.g.:
|
@@ -33,17 +56,22 @@ module Leto
|
|
33
56
|
# ["bar", [[:[], 1]], [[:[], 0]]]
|
34
57
|
# ]
|
35
58
|
def self.shared_mutables(obj1, obj2)
|
36
|
-
|
59
|
+
each_shared_object(obj1, obj2, filter: method(:mutable?)).to_a
|
37
60
|
end
|
38
61
|
|
39
62
|
def self.shared_objects(obj1, obj2, filter: nil)
|
40
|
-
|
41
|
-
|
42
|
-
|
63
|
+
each_shared_object(obj1, obj2, filter: filter).to_a
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.each_shared_object(obj1, obj2, filter: nil)
|
67
|
+
block_given? or return enum_for(__method__, obj1, obj2, filter: filter)
|
68
|
+
|
69
|
+
obj2_els_with_path = trace(obj2).to_a
|
70
|
+
trace(obj1).each do |el1, path1|
|
43
71
|
next if filter && !filter.call(el1)
|
44
72
|
|
45
|
-
|
46
|
-
|
73
|
+
obj2_els_with_path.reject do |el2, path2|
|
74
|
+
yield(el1, path1, path2) if el1.equal?(el2)
|
47
75
|
end
|
48
76
|
end
|
49
77
|
end
|
@@ -56,9 +84,26 @@ module Leto
|
|
56
84
|
IMMUTABLE_CLASSES = [
|
57
85
|
FalseClass,
|
58
86
|
Float,
|
59
|
-
Integer
|
87
|
+
if defined?(Integer)
|
88
|
+
Integer
|
89
|
+
else
|
90
|
+
Fixnum # rubocop:disable Lint/UnifiedInteger for Ruby < 2.4
|
91
|
+
end,
|
60
92
|
NilClass,
|
61
93
|
Symbol,
|
62
94
|
TrueClass,
|
63
95
|
].freeze
|
96
|
+
|
97
|
+
def self.duplicable?(obj)
|
98
|
+
!NON_DUPLICABLE_CLASSES.include?(obj.class) && obj.respond_to?(:dup)
|
99
|
+
end
|
100
|
+
private_class_method :duplicable?
|
101
|
+
|
102
|
+
require 'singleton'
|
103
|
+
|
104
|
+
NON_DUPLICABLE_CLASSES = [
|
105
|
+
Method,
|
106
|
+
Singleton,
|
107
|
+
UnboundMethod
|
108
|
+
].freeze
|
64
109
|
end
|
data/lib/leto/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: leto
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janosch Müller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -46,14 +46,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
46
|
requirements:
|
47
47
|
- - ">="
|
48
48
|
- !ruby/object:Gem::Version
|
49
|
-
version: 2.
|
49
|
+
version: 2.3.0
|
50
50
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
requirements: []
|
56
|
-
rubygems_version: 3.4.
|
56
|
+
rubygems_version: 3.4.13
|
57
57
|
signing_key:
|
58
58
|
specification_version: 4
|
59
59
|
summary: Generic object traverser
|