leto 1.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/leto.svg)](http://badge.fury.io/rb/leto)
|
4
|
-
[![Build Status](https://github.com/jaynetics/leto/workflows/tests/badge.svg)](https://github.com/jaynetics/leto/actions)
|
4
|
+
[![Build Status](https://github.com/jaynetics/leto/actions/workflows/tests.yml/badge.svg)](https://github.com/jaynetics/leto/actions)
|
5
|
+
[![Coverage](https://codecov.io/gh/jaynetics/leto/branch/main/graph/badge.svg?token=0993K9I8VC)](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
|