leto 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +28 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/Rakefile +12 -0
- data/benchmark.rb +11 -0
- data/leto.gemspec +25 -0
- data/lib/leto/call.rb +64 -0
- data/lib/leto/dig.rb +5 -0
- data/lib/leto/path.rb +51 -0
- data/lib/leto/utils.rb +64 -0
- data/lib/leto/version.rb +5 -0
- data/lib/leto.rb +7 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 539f52438c18bb93115734b5c3c9dd5f8678886d392357c4fdca658b43d95357
|
4
|
+
data.tar.gz: bbe092a276d3241d39857cb81e8fb748108b15e67cbc0e421b7390545b8a066a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9a0e610d258c35cf044c0118ccc10dff918edc8495df7448c058dbe6ad3201e540952fa67ac14e065229e1dd961788936e1343d30254743f47319609b5b4e169
|
7
|
+
data.tar.gz: c7e0fa4e9be9c2c0da092a7dbb88c78b9b11a12d8fb4f9d34e2ed97794ab2f26282c1fe31b04db5c70039198e675d2770ab0a7ae52edcb28049e7568c6c7bc56
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
relaxed-rubocop: .rubocop.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
NewCops: enable
|
6
|
+
SuggestExtensions: false
|
7
|
+
TargetRubyVersion: 2.6
|
8
|
+
|
9
|
+
Gemspec/RequiredRubyVersion:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Gemspec/RequireMFA:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Layout/LineLength:
|
16
|
+
Max: 80
|
17
|
+
|
18
|
+
Lint/AmbiguousBlockAssociation:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Lint/AmbiguousOperatorPrecedence:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Style/FetchEnvVar:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Style/FrozenStringLiteralComment:
|
28
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Janosch Müller
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Leto
|
2
|
+
|
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)
|
5
|
+
|
6
|
+
A generic object traverser for Ruby (named after the Greek [childbearing goddess Leto](https://www.theoi.com/Titan/TitanisLeto.html)).
|
7
|
+
|
8
|
+
Takes an object and recursively yields:
|
9
|
+
|
10
|
+
- the given object
|
11
|
+
- instance variables, class variables, constants
|
12
|
+
- Hash keys and values
|
13
|
+
- Enumerable members
|
14
|
+
- Struct members
|
15
|
+
- Range begins and ends
|
16
|
+
|
17
|
+
This makes stuff like deep-freezing fairly easy to implement:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
my_object = [ { a: ['b', 'c'..'d'] } ]
|
21
|
+
|
22
|
+
Leto.call(my_object, &:freeze)
|
23
|
+
|
24
|
+
my_object.frozen? # => true
|
25
|
+
my_object[0].frozen? # => true
|
26
|
+
my_object[0][:a][1].begin.frozen? # => true
|
27
|
+
```
|
28
|
+
|
29
|
+
Note: the slightly smarter `Leto.deep_freeze` is one of the [included utility methods](#included-utility-methods).
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
Basic example:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
object = [{ a: ['b', 'c'..'d'] }]
|
37
|
+
|
38
|
+
Leto.call(object) { |el| p el }
|
39
|
+
# prints:
|
40
|
+
#
|
41
|
+
# [{:a=>["b", ["c".."d"]]}]
|
42
|
+
# {:a=>["b", ["c".."d"]]}
|
43
|
+
# :a
|
44
|
+
# ["b", ["c".."d"]]
|
45
|
+
# "b"
|
46
|
+
# "c".."d"
|
47
|
+
# "c"
|
48
|
+
# "d"
|
49
|
+
|
50
|
+
Leto.call(object).to_a
|
51
|
+
# => [[{:a=>["b", ["c".."d"]]}], {:a=>["b", ["c".."d"]]}, :a, ...]
|
52
|
+
|
53
|
+
# all (sub-)objects have a path:
|
54
|
+
Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
|
55
|
+
# prints:
|
56
|
+
#
|
57
|
+
# [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
|
58
|
+
# {:a=>["b", "c".."d"]} @#<Leto::Path [{:a=>["b", "c".."d"]}][0]>
|
59
|
+
# :a @#<Leto::Path [{:a=>["b", "c".."d"]}][0].keys[0]>
|
60
|
+
# ["b", "c".."d"] @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a]>
|
61
|
+
# "b" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][0]>
|
62
|
+
# "c".."d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1]>
|
63
|
+
# "c" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].begin>
|
64
|
+
# "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
|
65
|
+
|
66
|
+
# paths can be looked up with Leto::Path#resolve or Leto::dig
|
67
|
+
path = Leto.call(object).map { |_el, path| path }.last # => #<Leto::Path...>
|
68
|
+
path.resolve # => "d"
|
69
|
+
Leto.dig(object, path) # => "d"
|
70
|
+
Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
|
71
|
+
```
|
72
|
+
|
73
|
+
### Included utility methods
|
74
|
+
|
75
|
+
- `Leto.deep_freeze(obj)`
|
76
|
+
- similar to the version above, but avoids freezing Modules and unfreezables
|
77
|
+
- `Leto.deep_print(obj)`
|
78
|
+
- for debugging - prints more information than `pretty_print` does by default
|
79
|
+
- `Leto.deep_eql?(obj1, obj2)`
|
80
|
+
- stricter version of `#eql?` that takes all ivars into consideration
|
81
|
+
- `Leto.shared_mutable_state?(obj1, obj2)`
|
82
|
+
- useful for debugging or verifying that a `#dup` implementation is sane
|
83
|
+
- `Leto.shared_mutables(obj1, obj2)`
|
84
|
+
- useful for debugging or verifying that a `#dup` implementation is sane
|
85
|
+
- `Leto.shared_objects(obj1, obj2)`
|
86
|
+
- returns all objects shared by `obj1` and `obj2`, whether mutable or not
|
87
|
+
|
88
|
+
## Benchmarks
|
89
|
+
|
90
|
+
```
|
91
|
+
Leto.deep_freeze: 8762.1 i/s
|
92
|
+
IceNine.deep_freeze: 7390.3 i/s - 1.19x (± 0.00) slower
|
93
|
+
```
|
94
|
+
|
95
|
+
## Contributing
|
96
|
+
|
97
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jaynetics/leto.
|
98
|
+
|
99
|
+
## License
|
100
|
+
|
101
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/benchmark.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'benchmark/ips'
|
2
|
+
require 'ice_nine'
|
3
|
+
require_relative 'lib/leto'
|
4
|
+
|
5
|
+
obj = (0..9).to_h { |n| [n, [{ false => true, sym: ('a'..'z').to_a.shuffle }]] }
|
6
|
+
|
7
|
+
Benchmark.ips do |x|
|
8
|
+
x.report('Leto.deep_freeze') { Leto.deep_freeze(obj) }
|
9
|
+
x.report('IceNine.deep_freeze') { IceNine.deep_freeze(obj) }
|
10
|
+
x.compare!
|
11
|
+
end; nil
|
data/leto.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/leto/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "leto"
|
7
|
+
spec.version = Leto::VERSION
|
8
|
+
spec.authors = ["Janosch Müller"]
|
9
|
+
spec.email = ["janosch84@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Generic object traverser"
|
12
|
+
spec.homepage = "https://github.com/jaynetics/leto"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
|
19
|
+
spec.files = Dir.chdir(__dir__) do
|
20
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
21
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
end
|
data/lib/leto/call.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Leto
|
2
|
+
LEAKY_PROCESS_TMS = RUBY_VERSION.to_f <= 2.7
|
3
|
+
|
4
|
+
def self.call(obj, &block)
|
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)
|
11
|
+
obj
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.traverse(obj, path, seen, block)
|
15
|
+
return if seen[obj]
|
16
|
+
|
17
|
+
seen[obj] = true
|
18
|
+
|
19
|
+
path ? block.call(obj, path) : block.call(obj)
|
20
|
+
|
21
|
+
obj.instance_variables.each do |ivar_name|
|
22
|
+
traverse(
|
23
|
+
obj.instance_variable_get(ivar_name),
|
24
|
+
path&.+([[:instance_variable_get, ivar_name]]),
|
25
|
+
seen, block
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
case obj
|
30
|
+
when Hash
|
31
|
+
obj.keys.each_with_index do |k, i|
|
32
|
+
traverse(k, path&.+([[:keys], [:[], i]]), seen, block)
|
33
|
+
traverse(obj[k], path&.+([[:[], k]]), seen, block)
|
34
|
+
end
|
35
|
+
when Module
|
36
|
+
obj.class_variables.each do |cvar_name|
|
37
|
+
traverse(
|
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|
|
44
|
+
traverse(
|
45
|
+
obj.const_get(const_name),
|
46
|
+
path&.+([[:const_get, const_name]]),
|
47
|
+
seen, block
|
48
|
+
)
|
49
|
+
end
|
50
|
+
when Range
|
51
|
+
traverse(obj.begin, path&.+([[:begin]]), seen, block)
|
52
|
+
traverse(obj.end, path&.+([[:end]]), seen, block)
|
53
|
+
when Struct
|
54
|
+
obj.members.each do |member|
|
55
|
+
traverse(obj[member], path&.+([[:[], member]]), seen, block)
|
56
|
+
end
|
57
|
+
when Enumerable
|
58
|
+
obj.each_with_index do |el, idx|
|
59
|
+
traverse(el, path&.+([[:[], idx]]), seen, block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
private_class_method :traverse
|
64
|
+
end
|
data/lib/leto/dig.rb
ADDED
data/lib/leto/path.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Light wrapper around Array, mostly for nicer display.
|
2
|
+
module Leto
|
3
|
+
class Path
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :start, :steps
|
7
|
+
|
8
|
+
def initialize(start:, steps: [])
|
9
|
+
@start = start
|
10
|
+
@steps = steps
|
11
|
+
end
|
12
|
+
|
13
|
+
def each(&block)
|
14
|
+
steps.each(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def +(other)
|
18
|
+
self.class.new(start: start, steps: steps + other.to_a)
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve
|
22
|
+
steps.inject(start) do |obj, (method, *args)|
|
23
|
+
obj&.send(method, *args) or break obj
|
24
|
+
rescue StandardError => e
|
25
|
+
warn "#{__method__}: #{e.class} #{e.message}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
other.to_a == steps
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
start_str = start.inspect
|
35
|
+
start_str = "#{start_str[0..38]}…#{start_str[-1]}" if start_str.size > 40
|
36
|
+
[
|
37
|
+
"#<#{self.class} #{start_str}",
|
38
|
+
steps.map do |method, *args|
|
39
|
+
args_str = args.map(&:inspect).join(', ')
|
40
|
+
if method == :[]
|
41
|
+
"[#{args_str}]"
|
42
|
+
else
|
43
|
+
".#{method}#{"(#{args_str})" unless args_str.empty?}"
|
44
|
+
end
|
45
|
+
end,
|
46
|
+
">"
|
47
|
+
].join
|
48
|
+
end
|
49
|
+
alias to_s inspect
|
50
|
+
end
|
51
|
+
end
|
data/lib/leto/utils.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Leto
|
2
|
+
def self.deep_freeze(obj, include_modules: false)
|
3
|
+
call(obj) do |el|
|
4
|
+
el.freeze if el.respond_to?(:freeze) &&
|
5
|
+
!el.frozen? &&
|
6
|
+
(include_modules || !el.is_a?(Module))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.deep_print(obj, print_method: :inspect, indent: 4, show_path: true)
|
11
|
+
call(obj) do |el, path|
|
12
|
+
puts "#{' ' * path.count * indent}#{el.send(print_method)}" \
|
13
|
+
"#{" @ #{path.inspect}" if show_path}" \
|
14
|
+
[0..78]
|
15
|
+
end
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.deep_eql?(obj1, obj2)
|
20
|
+
call(obj1).to_a == call(obj2).to_a
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.shared_mutable_state?(obj1, obj2)
|
24
|
+
shared_mutables(obj1, obj2).any?
|
25
|
+
end
|
26
|
+
|
27
|
+
# returns [[shared_object, path1, path2], ...], e.g.:
|
28
|
+
# str1 = 'foo'.dup
|
29
|
+
# str2 = 'bar'.dup
|
30
|
+
# shared_mutables([str1, str2], [str2, str1]) # =>
|
31
|
+
# [
|
32
|
+
# ["foo", [[:[], 0]], [[:[], 1]]],
|
33
|
+
# ["bar", [[:[], 1]], [[:[], 0]]]
|
34
|
+
# ]
|
35
|
+
def self.shared_mutables(obj1, obj2)
|
36
|
+
shared_objects(obj1, obj2, filter: method(:mutable?))
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.shared_objects(obj1, obj2, filter: nil)
|
40
|
+
objects_with_path1 = call(obj1).map { |el, path| [el, path] }
|
41
|
+
objects_with_path2 = call(obj2).map { |el, path| [el, path] }
|
42
|
+
objects_with_path1.each.with_object([]) do |(el1, path1), acc|
|
43
|
+
next if filter && !filter.call(el1)
|
44
|
+
|
45
|
+
objects_with_path2.reject do |el2, path2|
|
46
|
+
acc << [el1, path1, path2] if el1.equal?(el2)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.mutable?(obj)
|
52
|
+
!IMMUTABLE_CLASSES.include?(obj.class) && !obj.frozen?
|
53
|
+
end
|
54
|
+
private_class_method :mutable?
|
55
|
+
|
56
|
+
IMMUTABLE_CLASSES = [
|
57
|
+
FalseClass,
|
58
|
+
Float,
|
59
|
+
Integer,
|
60
|
+
NilClass,
|
61
|
+
Symbol,
|
62
|
+
TrueClass,
|
63
|
+
].freeze
|
64
|
+
end
|
data/lib/leto/version.rb
ADDED
data/lib/leto.rb
ADDED
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: leto
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Janosch Müller
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-02-24 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- janosch84@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rspec"
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- CHANGELOG.md
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- benchmark.rb
|
28
|
+
- leto.gemspec
|
29
|
+
- lib/leto.rb
|
30
|
+
- lib/leto/call.rb
|
31
|
+
- lib/leto/dig.rb
|
32
|
+
- lib/leto/path.rb
|
33
|
+
- lib/leto/utils.rb
|
34
|
+
- lib/leto/version.rb
|
35
|
+
homepage: https://github.com/jaynetics/leto
|
36
|
+
licenses:
|
37
|
+
- MIT
|
38
|
+
metadata:
|
39
|
+
changelog_uri: https://github.com/jaynetics/leto/blob/main/CHANGELOG.md
|
40
|
+
homepage_uri: https://github.com/jaynetics/leto
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 2.6.0
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.4.1
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Generic object traverser
|
60
|
+
test_files: []
|